Repository: swuecho/chat Branch: master Commit: cf9af2ce0a19 Files: 547 Total size: 23.7 MB Directory structure: gitextract_n1v0oway/ ├── .dockerignore ├── .github/ │ └── workflows/ │ ├── docker-image.yml │ ├── fly.yml │ ├── mobile-build.yml │ └── publish.yml ├── .gitignore ├── AGENTS.md ├── CLAUDE.md ├── Dockerfile ├── README.md ├── api/ │ ├── .air.toml │ ├── .github/ │ │ └── workflows/ │ │ └── go.yml │ ├── .gitignore │ ├── .vscode/ │ │ └── settings.json │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── admin_handler.go │ ├── ai/ │ │ └── model.go │ ├── artifact_instruction.txt │ ├── auth/ │ │ ├── auth.go │ │ ├── auth_test.go │ │ ├── token.go │ │ └── token_test.go │ ├── bot_answer_history_handler.go │ ├── bot_answer_history_service.go │ ├── chat_artifact.go │ ├── chat_auth_user_handler.go │ ├── chat_auth_user_service.go │ ├── chat_comment_handler.go │ ├── chat_comment_service.go │ ├── chat_main_handler.go │ ├── chat_main_service.go │ ├── chat_message_handler.go │ ├── chat_message_service.go │ ├── chat_message_service_test.go │ ├── chat_model_handler.go │ ├── chat_model_handler_test.go │ ├── chat_model_privilege_handler.go │ ├── chat_prompt_hander.go │ ├── chat_prompt_service.go │ ├── chat_prompt_service_test.go │ ├── chat_session_handler.go │ ├── chat_session_service.go │ ├── chat_session_service_test.go │ ├── chat_snapshot_handler.go │ ├── chat_snapshot_handler_test.go │ ├── chat_snapshot_service.go │ ├── chat_user_active_chat_session_handler.go │ ├── chat_user_active_chat_session_sevice.go │ ├── chat_workspace_handler.go │ ├── chat_workspace_service.go │ ├── constants.go │ ├── embed_debug_test.go │ ├── errors.go │ ├── file_upload_handler.go │ ├── file_upload_service.go │ ├── go.mod │ ├── go.sum │ ├── handle_tts.go │ ├── jwt_secret_service.go │ ├── llm/ │ │ ├── claude/ │ │ │ └── claude.go │ │ ├── gemini/ │ │ │ ├── gemini.go │ │ │ └── gemini_test.go │ │ └── openai/ │ │ ├── chat.go │ │ ├── client.go │ │ ├── common.go │ │ └── openai.go │ ├── llm_openai.go │ ├── llm_summary.go │ ├── main.go │ ├── main_test.go │ ├── middleware_authenticate.go │ ├── middleware_gzip.go │ ├── middleware_lastRequestTime.go │ ├── middleware_rateLimit.go │ ├── middleware_validation.go │ ├── model_claude3_service.go │ ├── model_completion_service.go │ ├── model_custom_service.go │ ├── model_gemini_service.go │ ├── model_ollama_service.go │ ├── model_openai_service.go │ ├── model_test_service.go │ ├── models/ │ │ └── models.go │ ├── models.go │ ├── openai_test.go │ ├── pre-commit.sh │ ├── sqlc/ │ │ ├── README.txt │ │ ├── queries/ │ │ │ ├── auth_user.sql │ │ │ ├── auth_user_management.sql │ │ │ ├── bot_answer_history.sql │ │ │ ├── chat_comment.sql │ │ │ ├── chat_file.sql │ │ │ ├── chat_log.sql │ │ │ ├── chat_message.sql │ │ │ ├── chat_model.sql │ │ │ ├── chat_prompt.sql │ │ │ ├── chat_session.sql │ │ │ ├── chat_snapshot.sql │ │ │ ├── chat_workspace.sql │ │ │ ├── jwt_secrets.sql │ │ │ ├── user_active_chat_session.sql │ │ │ └── user_chat_model_privilege.sql │ │ └── schema.sql │ ├── sqlc.yaml │ ├── sqlc_queries/ │ │ ├── auth_user.sql.go │ │ ├── auth_user_management.sql.go │ │ ├── bot_answer_history.sql.go │ │ ├── chat_comment.sql.go │ │ ├── chat_file.sql.go │ │ ├── chat_log.sql.go │ │ ├── chat_message.sql.go │ │ ├── chat_model.sql.go │ │ ├── chat_prompt.sql.go │ │ ├── chat_session.sql.go │ │ ├── chat_snapshot.sql.go │ │ ├── chat_workspace.sql.go │ │ ├── db.go │ │ ├── jwt_secrets.sql.go │ │ ├── models.go │ │ ├── user_active_chat_session.sql.go │ │ ├── user_chat_model_privilege.sql.go │ │ ├── zz_custom_method.go │ │ └── zz_custom_query.go │ ├── static/ │ │ ├── awesome-chatgpt-prompts-en.json │ │ ├── awesome-chatgpt-prompts-zh.json │ │ └── static.go │ ├── streaming_helpers.go │ ├── test_build │ ├── text_buffer.go │ ├── tools/ │ │ ├── apply_a_similar_change/ │ │ │ ├── README.md │ │ │ ├── apply_diff.py │ │ │ ├── apply_diff_uselib.py │ │ │ ├── parse_diff.py │ │ │ ├── parse_diff2.py │ │ │ ├── parse_diff3.py │ │ │ └── stream.diff │ │ └── fix_eris.py │ ├── util.go │ ├── util_test.go │ └── util_words_test.go ├── artifacts.md ├── chat.code-workspace ├── docker-compose.yaml ├── docs/ │ ├── add_model_en.md │ ├── add_model_zh.md │ ├── artifact_gallery_en.md │ ├── artifact_gallery_zh.md │ ├── code_runner_artifacts_tutorial.md │ ├── code_runner_capabilities.md │ ├── code_runner_csv_tutorial.md │ ├── custom_model_api_en.md │ ├── deployment_en.md │ ├── deployment_zh.md │ ├── dev/ │ │ ├── ERROR_HANDLING_STANDARDS.md │ │ ├── INTEGRATION_GUIDE.md │ │ ├── code_runner_manual.md │ │ ├── conversation_patch_example.js │ │ ├── conversation_vfs_integration.md │ │ ├── python_async_execution.md │ │ ├── sse_processing_logic.md │ │ ├── vfs_integration_example.md │ │ ├── virtual_file_system_plan.md │ │ └── virtual_file_system_usage.md │ ├── dev_locally_en.md │ ├── dev_locally_zh.md │ ├── ollama_en.md │ ├── ollama_zh.md │ ├── prompts.md │ ├── snapshots_vs_chatbots_en.md │ ├── snapshots_vs_chatbots_zh.md │ ├── tool_use_code_runner.md │ └── tool_use_showcase.md ├── e2e/ │ ├── .gitignore │ ├── LICENSE │ ├── Makefile │ ├── lib/ │ │ ├── button-helpers.ts │ │ ├── chat-test-setup.ts │ │ ├── db/ │ │ │ ├── chat_message/ │ │ │ │ └── index.ts │ │ │ ├── chat_model/ │ │ │ │ └── index.ts │ │ │ ├── chat_prompt/ │ │ │ │ └── index.ts │ │ │ ├── chat_session/ │ │ │ │ └── index.ts │ │ │ ├── chat_workspace/ │ │ │ │ └── index.ts │ │ │ ├── config.ts │ │ │ └── user/ │ │ │ └── index.ts │ │ ├── message-helpers.ts │ │ └── sample.ts │ ├── package.json │ ├── playwright.config.ts │ ├── tests/ │ │ ├── 00_chat_gpt_web.spec.ts │ │ ├── 01_register.spec.ts │ │ ├── 02_simpe_prompt.spec.ts │ │ ├── 03_chat_session.spec.ts │ │ ├── 04_simpe_prompt_and_message.spec.ts │ │ ├── 05_chat_session.spec.ts │ │ ├── 06_clear_messages.spec.ts │ │ ├── 07_set_session_max_len.spec.ts │ │ ├── 08_session_config.spec.ts │ │ ├── 09_session_answer.spec.ts │ │ ├── 10_session_answer_regenerate.spec.ts │ │ ├── 10_session_answer_regenerate_fixed.spec.ts │ │ └── 11_workspace.spec.ts │ └── tests-examples/ │ └── demo-todo-app.spec.ts ├── fly.toml ├── mobile/ │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── android/ │ │ ├── .gitignore │ │ ├── app/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── debug/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── example/ │ │ │ │ │ └── chat_mobile/ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values/ │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night/ │ │ │ │ └── styles.xml │ │ │ └── profile/ │ │ │ └── AndroidManifest.xml │ │ ├── build.gradle.kts │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ └── settings.gradle.kts │ ├── devtools_options.yaml │ ├── ios/ │ │ ├── .gitignore │ │ ├── Flutter/ │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ └── Release.xcconfig │ │ ├── Podfile │ │ ├── Runner/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ ├── Contents.json │ │ │ │ └── README.md │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── Runner-Bridging-Header.h │ │ ├── Runner.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata/ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Runner.xcscheme │ │ ├── Runner.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ │ └── RunnerTests/ │ │ └── RunnerTests.swift │ ├── lib/ │ │ ├── api/ │ │ │ ├── api_config.dart │ │ │ ├── api_exception.dart │ │ │ └── chat_api.dart │ │ ├── constants/ │ │ │ └── chat.dart │ │ ├── main.dart │ │ ├── models/ │ │ │ ├── auth_token_result.dart │ │ │ ├── chat_message.dart │ │ │ ├── chat_model.dart │ │ │ ├── chat_session.dart │ │ │ ├── chat_snapshot.dart │ │ │ ├── suggestions_response.dart │ │ │ └── workspace.dart │ │ ├── screens/ │ │ │ ├── auth_gate.dart │ │ │ ├── chat_screen.dart │ │ │ ├── home_screen.dart │ │ │ ├── login_screen.dart │ │ │ ├── snapshot_list_screen.dart │ │ │ └── snapshot_screen.dart │ │ ├── state/ │ │ │ ├── auth_provider.dart │ │ │ ├── message_provider.dart │ │ │ ├── model_provider.dart │ │ │ ├── session_provider.dart │ │ │ └── workspace_provider.dart │ │ ├── theme/ │ │ │ ├── app_theme.dart │ │ │ └── color_utils.dart │ │ ├── utils/ │ │ │ ├── api_error.dart │ │ │ └── thinking_parser.dart │ │ └── widgets/ │ │ ├── icon_map.dart │ │ ├── message_bubble.dart │ │ ├── message_composer.dart │ │ ├── session_tile.dart │ │ ├── suggested_questions.dart │ │ ├── thinking_section.dart │ │ └── workspace_selector.dart │ ├── linux/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── flutter/ │ │ │ ├── CMakeLists.txt │ │ │ ├── generated_plugin_registrant.cc │ │ │ ├── generated_plugin_registrant.h │ │ │ └── generated_plugins.cmake │ │ └── runner/ │ │ ├── CMakeLists.txt │ │ ├── main.cc │ │ ├── my_application.cc │ │ └── my_application.h │ ├── macos/ │ │ ├── .gitignore │ │ ├── Flutter/ │ │ │ ├── Flutter-Debug.xcconfig │ │ │ ├── Flutter-Release.xcconfig │ │ │ └── GeneratedPluginRegistrant.swift │ │ ├── Podfile │ │ ├── Runner/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── MainMenu.xib │ │ │ ├── Configs/ │ │ │ │ ├── AppInfo.xcconfig │ │ │ │ ├── Debug.xcconfig │ │ │ │ ├── Release.xcconfig │ │ │ │ └── Warnings.xcconfig │ │ │ ├── DebugProfile.entitlements │ │ │ ├── Info.plist │ │ │ ├── MainFlutterWindow.swift │ │ │ └── Release.entitlements │ │ ├── Runner.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ └── xcshareddata/ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Runner.xcscheme │ │ ├── Runner.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── RunnerTests/ │ │ └── RunnerTests.swift │ ├── pubspec.yaml │ ├── test/ │ │ └── widget_test.dart │ ├── web/ │ │ ├── index.html │ │ └── manifest.json │ └── windows/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter/ │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ └── runner/ │ ├── CMakeLists.txt │ ├── Runner.rc │ ├── flutter_window.cpp │ ├── flutter_window.h │ ├── main.cpp │ ├── resource.h │ ├── runner.exe.manifest │ ├── utils.cpp │ ├── utils.h │ ├── win32_window.cpp │ └── win32_window.h ├── scripts/ │ ├── branch_clean.py │ ├── locale_missing_key.py │ ├── merge_keys.py │ └── remove_older_branch.py └── web/ ├── .commitlintrc.json ├── .editorconfig ├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── docker-compose/ │ ├── docker-compose.yml │ ├── nginx/ │ │ └── nginx.conf │ └── readme.md ├── docs/ │ └── code_runner.md ├── index.html ├── license ├── package.json ├── postcss.config.js ├── public/ │ ├── awesome-chatgpt-prompts-en.json │ └── awesome-chatgpt-prompts-zh.json ├── rsbuild.config.ts ├── src/ │ ├── App.vue │ ├── api/ │ │ ├── admin.ts │ │ ├── bot_answer_history.ts │ │ ├── chat_active_user_session.ts │ │ ├── chat_file.ts │ │ ├── chat_instructions.ts │ │ ├── chat_message.ts │ │ ├── chat_model.ts │ │ ├── chat_prompt.ts │ │ ├── chat_session.ts │ │ ├── chat_snapshot.ts │ │ ├── chat_user_model_privilege.ts │ │ ├── chat_workspace.ts │ │ ├── comment.ts │ │ ├── content.ts │ │ ├── export.ts │ │ ├── index.ts │ │ ├── token.ts │ │ ├── use_chat_session.ts │ │ └── user.ts │ ├── assets/ │ │ └── recommend.json │ ├── components/ │ │ ├── admin/ │ │ │ ├── ModelCard.vue │ │ │ ├── SessionSnapshotModal.vue │ │ │ └── UserAnalysisModal.vue │ │ ├── common/ │ │ │ ├── EnhancedNotification.vue │ │ │ ├── HoverButton/ │ │ │ │ ├── Button.vue │ │ │ │ └── index.vue │ │ │ ├── NaiveProvider/ │ │ │ │ └── index.vue │ │ │ ├── NotificationDemo.vue │ │ │ ├── PromptStore/ │ │ │ │ └── index.vue │ │ │ ├── Setting/ │ │ │ │ ├── Admin.vue │ │ │ │ ├── General.vue │ │ │ │ └── index.vue │ │ │ ├── SvgIcon/ │ │ │ │ └── index.vue │ │ │ ├── UserAvatar/ │ │ │ │ └── index.vue │ │ │ └── index.ts │ │ └── custom/ │ │ ├── GithubSite.vue │ │ └── index.ts │ ├── config/ │ │ └── api.ts │ ├── constants/ │ │ ├── apiTypes.ts │ │ └── chat.ts │ ├── hooks/ │ │ ├── useBasicLayout.ts │ │ ├── useChatModels.ts │ │ ├── useCopyCode.ts │ │ ├── useIconRender.ts │ │ ├── useLanguage.ts │ │ ├── useOnlineStatus.ts │ │ ├── useTheme.ts │ │ └── useWorkspaceRouting.ts │ ├── icons/ │ │ ├── 403.vue │ │ └── 500.vue │ ├── locales/ │ │ ├── en-US-more.json │ │ ├── en-US.json │ │ ├── en.ts │ │ ├── index.ts │ │ ├── zh-CN.json │ │ ├── zh-TW-more.json │ │ └── zh-TW.json │ ├── main.ts │ ├── plugins/ │ │ ├── assets.ts │ │ └── index.ts │ ├── router/ │ │ ├── index.ts │ │ └── permission.ts │ ├── service/ │ │ └── snapshot.ts │ ├── services/ │ │ └── codeTemplates.ts │ ├── store/ │ │ ├── index.ts │ │ └── modules/ │ │ ├── app/ │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ ├── auth/ │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── message/ │ │ │ └── index.ts │ │ ├── prompt/ │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ ├── session/ │ │ │ └── index.ts │ │ ├── user/ │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ └── workspace/ │ │ └── index.ts │ ├── styles/ │ │ ├── global.less │ │ └── lib/ │ │ ├── github-markdown.less │ │ ├── highlight.less │ │ └── tailwind.css │ ├── types/ │ │ └── chat-models.ts │ ├── typings/ │ │ ├── chat.d.ts │ │ └── global.d.ts │ ├── utils/ │ │ ├── __tests__/ │ │ │ └── date.test.ts │ │ ├── artifacts.ts │ │ ├── crypto/ │ │ │ └── index.ts │ │ ├── date.ts │ │ ├── download.ts │ │ ├── errorHandler.ts │ │ ├── format/ │ │ │ └── index.ts │ │ ├── is/ │ │ │ └── index.ts │ │ ├── jwt.ts │ │ ├── logger.ts │ │ ├── notificationManager.ts │ │ ├── prompt.ts │ │ ├── rand.ts │ │ ├── request/ │ │ │ ├── axios.ts │ │ │ └── index.ts │ │ ├── sanitize.ts │ │ ├── storage/ │ │ │ ├── index.ts │ │ │ └── local.ts │ │ ├── string.ts │ │ ├── tooling.ts │ │ └── workspaceUrls.ts │ └── views/ │ ├── admin/ │ │ ├── index.vue │ │ ├── model/ │ │ │ ├── AddModelForm.vue │ │ │ └── index.vue │ │ ├── modelRateLimit/ │ │ │ ├── addChatModelForm.vue │ │ │ └── index.vue │ │ └── user/ │ │ └── index.vue │ ├── bot/ │ │ ├── all.vue │ │ ├── components/ │ │ │ ├── AnswerHistory.vue │ │ │ └── Message/ │ │ │ ├── index.vue │ │ │ └── style.less │ │ └── page.vue │ ├── chat/ │ │ ├── components/ │ │ │ ├── ArtifactGallery.vue │ │ │ ├── AudioPlayer/ │ │ │ │ └── index.vue │ │ │ ├── Conversation.vue │ │ │ ├── HeaderMobile/ │ │ │ │ └── index.vue │ │ │ ├── JumpToBottom.vue │ │ │ ├── Message/ │ │ │ │ ├── ArtifactContent.vue │ │ │ │ ├── ArtifactEditor.vue │ │ │ │ ├── ArtifactHeader.vue │ │ │ │ ├── ArtifactViewer.vue │ │ │ │ ├── ArtifactViewerBase.vue │ │ │ │ ├── SuggestedQuestions.vue │ │ │ │ └── index.vue │ │ │ ├── MessageList.vue │ │ │ ├── ModelSelector.vue │ │ │ ├── PromptGallery/ │ │ │ │ ├── PromptCards.vue │ │ │ │ └── index.vue │ │ │ ├── RenderMessage.vue │ │ │ ├── Session/ │ │ │ │ └── SessionConfig.vue │ │ │ ├── UploadModal.vue │ │ │ ├── Uploader.vue │ │ │ ├── UploaderReadOnly.vue │ │ │ ├── WorkspaceSelector/ │ │ │ │ ├── WorkspaceCard.vue │ │ │ │ ├── WorkspaceManagementModal.vue │ │ │ │ ├── WorkspaceModal.vue │ │ │ │ └── index.vue │ │ │ ├── __tests__/ │ │ │ │ └── modelSelectorUtils.test.ts │ │ │ └── modelSelectorUtils.ts │ │ ├── composables/ │ │ │ ├── README.md │ │ │ ├── useChatActions.ts │ │ │ ├── useConversationFlow.ts │ │ │ ├── useErrorHandling.ts │ │ │ ├── usePerformanceOptimizations.ts │ │ │ ├── useRegenerate.ts │ │ │ ├── useSearchAndPrompts.ts │ │ │ ├── useStreamHandling.ts │ │ │ └── useValidation.ts │ │ ├── hooks/ │ │ │ ├── useChat.ts │ │ │ ├── useCopyCode.ts │ │ │ ├── useScroll.ts │ │ │ ├── useSlashToFocus.ts │ │ │ └── useUsingContext.ts │ │ ├── index.vue │ │ └── layout/ │ │ ├── Layout.vue │ │ ├── index.ts │ │ └── sider/ │ │ ├── Footer.vue │ │ ├── List.vue │ │ └── index.vue │ ├── components/ │ │ ├── Avatar/ │ │ │ ├── MessageAvatar.vue │ │ │ └── ModelAvatar.vue │ │ ├── Message/ │ │ │ ├── AnswerContent.vue │ │ │ ├── Text.vue │ │ │ ├── ThinkingRenderer.vue │ │ │ ├── Util.ts │ │ │ ├── style.less │ │ │ ├── thinkingParser.ts │ │ │ ├── types/ │ │ │ │ └── thinking.ts │ │ │ └── useThinkingContent.ts │ │ └── Permission.vue │ ├── exception/ │ │ ├── 404/ │ │ │ └── index.vue │ │ └── 500/ │ │ └── index.vue │ ├── prompt/ │ │ ├── components/ │ │ │ ├── Definitions.vue │ │ │ ├── PromptCreator.vue │ │ │ └── PromptProcess.vue │ │ └── creator.vue │ └── snapshot/ │ ├── all.vue │ ├── components/ │ │ ├── Comment/ │ │ │ └── index.vue │ │ ├── Header/ │ │ │ └── index.vue │ │ ├── Message/ │ │ │ ├── index.vue │ │ │ └── style.less │ │ └── Search.vue │ └── page.vue ├── tailwind.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ **/node_modules **/dist ================================================ FILE: .github/workflows/docker-image.yml ================================================ name: e2e test on: push: branches: "**" pull_request: branches: "**" env: PG_DB: postgres PG_USER: postgres PG_HOST: localhost PG_PASS: thisisapassword jobs: build_chat: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: check locale run: python scripts/locale_missing_key.py web/src/locales --base zh-CN - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x" - name: build web run: | npm install npm run test npm run build working-directory: web - name: copy to api/static run: | cp -R web/dist/* api/static/ - name: Set up Go uses: actions/setup-go@v3 with: go-version: 1.19 - name: Build API run: go build -v ./... working-directory: api - name: Test API run: go test -v ./... working-directory: api - name: Build Chat image run: | docker build . --file Dockerfile -t ghcr.io/swuecho/chat:${GITHUB_SHA} docker tag ghcr.io/swuecho/chat:${GITHUB_SHA} ghcr.io/swuecho/chat:latest - name: docker compose run: docker compose up -d - name: show docker ps run: docker compose ps - name: show docker logs run: docker compose logs # Setup cache for node_modules - name: Cache node modules uses: actions/cache@v3 with: path: e2e/node_modules key: ${{ runner.os }}-node-${{ hashFiles('e2e/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies run: npm ci working-directory: e2e - name: Install playwright browsers run: npx playwright install --with-deps working-directory: e2e - run: npx playwright test working-directory: e2e ================================================ FILE: .github/workflows/fly.yml ================================================ name: Fly Deploy on: push: branches: - master jobs: deploy: name: Deploy app runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: superfly/flyctl-actions/setup-flyctl@master - run: flyctl deploy --remote-only env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} ================================================ FILE: .github/workflows/mobile-build.yml ================================================ name: Build Mobile Package on: workflow_dispatch: push: paths: - "mobile/**" - ".github/workflows/mobile-build.yml" pull_request: paths: - "mobile/**" - ".github/workflows/mobile-build.yml" jobs: build-android: runs-on: ubuntu-latest defaults: run: working-directory: mobile steps: - uses: actions/checkout@v3 - name: Set up Flutter uses: subosito/flutter-action@v2 with: channel: "stable" flutter-version: "3.35.4" - name: Install dependencies run: flutter pub get - name: Build Android APK run: flutter build apk --release - name: Build Android App Bundle run: flutter build appbundle --release - name: Upload Android artifacts uses: actions/upload-artifact@v4 with: name: chat-mobile-android path: | mobile/build/app/outputs/flutter-apk/app-release.apk mobile/build/app/outputs/bundle/release/app-release.aab build-ios: runs-on: macos-latest defaults: run: working-directory: mobile steps: - uses: actions/checkout@v3 - name: Set up Flutter uses: subosito/flutter-action@v2 with: channel: "stable" flutter-version: "3.35.4" - name: Install dependencies run: flutter pub get - name: Build iOS app (no codesign) run: flutter build ios --simulator - name: Upload iOS artifact uses: actions/upload-artifact@v4 with: name: chat-mobile-ios path: mobile/build/ios/iphonesimulator/Runner.app ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: push: tags: - "v*" jobs: build_api: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x" - name: build web run: | npm install npm run test npm run build working-directory: web - name: copy to api/static run: | cp -R web/dist/* api/static/ - name: Set up Go uses: actions/setup-go@v3 with: go-version: 1.24 - name: Build Chat Binary run: go build -v ./... working-directory: api - name: Test Chat run: go test -v ./... working-directory: api # use root folder docker - name: Build Chat image run: | docker build . --file Dockerfile -t ghcr.io/swuecho/chat:${GITHUB_REF#refs/tags/} - name: Login to GitHub Container Registry run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Push API image to GitHub Container Registry run: | docker push ghcr.io/swuecho/chat:${GITHUB_REF#refs/tags/} docker tag ghcr.io/swuecho/chat:${GITHUB_REF#refs/tags/} ghcr.io/swuecho/chat:latest docker push ghcr.io/swuecho/chat:latest - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: push to docker run: | docker tag ghcr.io/swuecho/chat:${GITHUB_REF#refs/tags/} echowuhao/chat:${GITHUB_REF#refs/tags/} docker tag ghcr.io/swuecho/chat:${GITHUB_REF#refs/tags/} echowuhao/chat:latest docker push echowuhao/chat:${GITHUB_REF#refs/tags/} docker push echowuhao/chat:latest ================================================ FILE: .gitignore ================================================ .DS_Store .env* env.sh env.ps data .python-version .aider* ================================================ FILE: AGENTS.md ================================================ # Chat - Multi-LLM Chat Interface A full-stack chat application that provides a unified interface for interacting with multiple large language models (LLMs) including OpenAI, Claude, Gemini, and Ollama. ## Project Overview This project is a ChatGPT wrapper that extends beyond OpenAI to support multiple LLM providers. It features a Vue.js frontend with a Go backend, using PostgreSQL for data persistence. ### Key Features - **Multi-LLM Support**: OpenAI, Claude, Gemini, and Ollama models - **Chat Sessions**: Persistent conversation history and context management - **Workspaces**: Organize chat sessions into customizable workspaces with colors and icons - **User Management**: Authentication, rate limiting, and admin controls - **File Uploads**: Support for text and multimedia files (model-dependent) - **Snapshots**: Shareable conversation snapshots with full-text search - **Prompt Management**: Built-in prompt templates with '/' shortcut - **Internationalization**: Support for multiple languages ## Architecture ### Frontend (Vue.js) - **Framework**: Vue 3 with Composition API - **State Management**: Pinia - **UI Library**: Naive UI - **Routing**: Vue Router - **Build Tool**: Rsbuild - **Styling**: Tailwind CSS + Less ### Backend (Go) - **Framework**: Standard Go HTTP with Gorilla Mux - **Database**: PostgreSQL with SQLC for type-safe queries - **Authentication**: JWT tokens - **Rate Limiting**: Built-in rate limiting (100 calls/10min default) - **File Upload**: Support for various file types ### Database (PostgreSQL) - **Schema**: Located in `api/sqlc/schema.sql` - **Queries**: Type-safe queries generated by SQLC - **Tables**: Users, sessions, messages, models, prompts, snapshots, workspaces, etc. ## Project Structure ``` chat/ ├── api/ # Go backend │ ├── main.go # Application entry point │ ├── llm/ # LLM provider integrations │ │ ├── openai/ # OpenAI integration │ │ ├── claude/ # Claude integration │ │ └── gemini/ # Gemini integration │ ├── sqlc/ # Database schema and queries │ ├── sqlc_queries/ # Generated type-safe queries │ └── static/ # Static assets ├── web/ # Vue.js frontend │ ├── src/ │ │ ├── components/ # Reusable components │ │ ├── views/ # Page components │ │ ├── store/ # Pinia stores │ │ ├── api/ # API client │ │ └── utils/ # Utility functions │ └── dist/ # Built frontend assets ├── docs/ # Documentation ├── e2e/ # End-to-end tests └── data/ # Database dumps ``` ## Development Setup ### Prerequisites - Go 1.19+ - Node.js 18+ - PostgreSQL - (Optional) Docker ### Backend Setup ```bash cd api go mod tidy go run main.go ``` ### Frontend Setup ```bash cd web npm install npm run dev ``` ### server reload both frontend and backend will be auto-reload whe code change. ### Database Setup 1. Create a PostgreSQL database 2. Run the schema from `api/sqlc/schema.sql` 3. Configure database connection in environment variables ## Configuration ### Environment Variables - `OPENAI_API_KEY`: OpenAI API key - `CLAUDE_API_KEY`: Claude API key - `GEMINI_API_KEY`: Gemini API key - `DATABASE_URL`: PostgreSQL connection string - `JWT_SECRET`: JWT signing secret - `OPENAI_RATELIMIT`: Rate limit (default: 100) ## API Endpoints ### Authentication - `POST /api/auth/login` - User login - `POST /api/auth/register` - User registration - `POST /api/auth/refresh` - Token refresh ### Chat - `GET /api/chat/sessions` - Get user sessions - `POST /api/chat/sessions` - Create new session - `GET /api/chat/messages` - Get session messages - `POST /api/chat/messages` - Send message - `DELETE /api/chat/sessions/:id` - Delete session ### Workspaces - `GET /api/workspaces` - Get user workspaces - `POST /api/workspaces` - Create new workspace - `GET /api/workspaces/{uuid}` - Get workspace by UUID - `PUT /api/workspaces/{uuid}` - Update workspace - `DELETE /api/workspaces/{uuid}` - Delete workspace - `PUT /api/workspaces/{uuid}/reorder` - Update workspace order - `PUT /api/workspaces/{uuid}/set-default` - Set default workspace - `POST /api/workspaces/{uuid}/sessions` - Create session in workspace - `GET /api/workspaces/{uuid}/sessions` - Get sessions in workspace - `POST /api/workspaces/default` - Ensure default workspace exists ### Models - `GET /api/models` - List available models - `POST /api/models` - Add new model (admin) - `PUT /api/models/:id` - Update model (admin) ### File Upload - `POST /api/files` - Upload file - `GET /api/files/:id` - Get file ## Key Components ### Frontend Components - `chat/index.vue`: Main chat interface - `chat/components/Message/`: Message rendering - `chat/components/Session/`: Session management - `admin/index.vue`: Admin dashboard - `prompt/creator.vue`: Prompt management ### Backend Handlers - `chat_main_handler.go`: Core chat functionality - `chat_session_handler.go`: Session management - `chat_message_handler.go`: Message handling - `chat_workspace_handler.go`: Workspace management - `auth_handler.go`: Authentication - `admin_handler.go`: Admin operations ## Testing ### Backend Tests ```bash cd api go test ./... ``` ### Frontend Tests ```bash cd e2e npm test ``` ### E2E Tests ```bash cd e2e npm test ``` ## Workspaces The application supports organizing chat sessions into workspaces for better organization and context management. ### Workspace Features - **Custom Organization**: Create themed workspaces for different projects or topics - **Visual Customization**: Set custom colors and icons for easy identification - **Session Management**: Sessions are automatically associated with workspaces - **Default Workspace**: Each user has a default "General" workspace that's created automatically - **Ordering**: Workspaces can be reordered for personal preference - **Permission Control**: Users can only access their own workspaces ### Workspace Properties - **Name**: Human-readable workspace name - **Description**: Optional description for workspace purpose - **Color**: Hex color code for visual theming (default: #6366f1) - **Icon**: Icon identifier for visual representation (default: folder) - **Default**: Boolean flag indicating if this is the user's default workspace - **Order Position**: Integer for custom workspace ordering ### Database Schema The `chat_workspace` table includes: - `id`: Primary key - `uuid`: Unique identifier for API operations - `user_id`: Foreign key to owner user - `name`: Workspace name (required) - `description`: Optional description (default: empty string) - `color`: Hex color code (default: #6366f1) - `icon`: Icon identifier (default: folder) - `is_default`: Boolean default flag - `order_position`: Integer for ordering - `created_at`/`updated_at`: Timestamps Sessions are linked to workspaces via the `workspace_id` foreign key in the `chat_session` table. ## Model Support - **OpenAI**: GPT-3.5, GPT-4 models - **Claude**: Claude 3 Opus, Sonnet, Haiku, Claude 4 - **Gemini**: Gemini Pro - **Ollama**: Local model hosting support ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Multi-LLM chat interface with Vue.js frontend, Go backend, and PostgreSQL database. Supports OpenAI, Claude, Gemini, and Ollama models with features like workspaces, snapshots, and file uploads. ## Development Commands ### Backend (Go) ```bash cd api # Install dependencies and hot-reload tool make install # Run server with hot reload (uses Air) make serve # Build make build # Format code make fmt # Run tests go test ./... # Regenerate SQLC code (after modifying queries or schema) sqlc generate ``` **Important**: The backend uses Air for hot-reloading during development. Configuration is in `api/.air.toml`. ### Frontend (Vue.js) ```bash cd web # Install dependencies npm install # Development server with hot reload npm run dev # Build for production npm run build # Run linter npm run lint # Fix linting issues npm run lint:fix # Run tests npm test ``` ### E2E Tests (Playwright) ```bash cd e2e # Run all tests npx playwright test # Run with UI npx playwright test --ui ``` ## Architecture ### Request Flow ``` HTTP Request → Mux Router → Handler → Service → SQLC Queries → PostgreSQL ↓ LLM Provider (OpenAI/Claude/Gemini/Ollama) ``` ### Backend Architecture (Go) **Key Pattern**: The backend follows a handler → service → repository (SQLC) pattern: 1. **Handlers** (`*_handler.go`): HTTP request/response handling - `chat_main_handler.go`: Core chat functionality - `chat_session_handler.go`: Session CRUD operations - `chat_message_handler.go`: Message operations - `chat_workspace_handler.go`: Workspace management - `chat_auth_user_handler.go`: Authentication - `admin_handler.go`: Admin operations 2. **Services** (`*_service.go`): Business logic layer - `chat_main_service.go`: Chat orchestration and LLM routing 3. **SQLC Generated Code** (`sqlc_queries/`): Type-safe database queries - Schema: `api/sqlc/schema.sql` - Queries: `api/sqlc/queries/*.sql` - Generated Go: `api/sqlc_queries/*.go` - Config: `api/sqlc.yaml` 4. **LLM Integrations** (`llm/`): - `llm/openai/`: OpenAI API client - `llm/claude/`: Claude API client - `llm/gemini/`: Gemini API client - Each provider has its own request/response formatting **Router**: Uses Gorilla Mux for routing (configured in `main.go`) ### Frontend Architecture (Vue.js) **Stack**: Vue 3 (Composition API) + Pinia + Naive UI + Rsbuild + Tailwind CSS **Key Directories**: - `web/src/views/`: Page components - `web/src/components/`: Reusable components - `web/src/store/modules/`: Pinia stores for state management - `web/src/api/`: API client functions - `web/src/views/chat/composables/`: Chat feature composables (refactored from monolithic component) **Chat Composables Pattern**: The main chat interface uses a composable-based architecture for better separation of concerns: - `useStreamHandling.ts`: Handles LLM streaming responses - `useConversationFlow.ts`: Manages conversation lifecycle - `useRegenerate.ts`: Message regeneration - `useSearchAndPrompts.ts`: Search and prompt templates - `useChatActions.ts`: Snapshot, bot creation, file uploads - `useErrorHandling.ts`: Centralized error management - `useValidation.ts`: Input validation rules - `usePerformanceOptimizations.ts`: Debouncing, memoization This pattern reduced the main component from 738 to 293 lines while adding better error handling and type safety. ### Database (PostgreSQL + SQLC) **SQLC Workflow**: 1. Define schema in `api/sqlc/schema.sql` 2. Write SQL queries in `api/sqlc/queries/*.sql` 3. Run `sqlc generate` to create type-safe Go code 4. Use generated code in services **Key Tables**: - `auth_user`: User accounts (first registered user becomes admin) - `chat_session`: Chat sessions - `chat_message`: Messages within sessions - `chat_workspace`: Workspace organization - `chat_model`: Available LLM models - `chat_prompt`: Prompt templates - `chat_snapshot`: Shareable conversation snapshots - `chat_file`: File uploads **Default Context**: Latest 4 messages are included in context by default. ## Environment Variables Required variables (set in shell or `.env`): ```bash # Database (required) DATABASE_URL=postgres://user:pass@host:port/dbname?sslmode=disable # LLM API Keys (at least one required) OPENAI_API_KEY=sk-... CLAUDE_API_KEY=... GEMINI_API_KEY=... DEEPSEEK_API_KEY=... # Optional OPENAI_RATELIMIT=100 # Calls per 10 minutes (default: 100) JWT_SECRET=... # For JWT token signing ``` **Note**: The "debug" model doesn't require API keys for testing. ## Key Features & Patterns ### Authentication & Authorization - JWT-based authentication - First registered user becomes administrator (`is_superuser=true`) - Rate limiting per user (default: 100 calls/10min, configurable via `OPENAI_RATELIMIT`) - Per-model rate limiting available for specific models (GPT-4, etc.) ### Workspaces - Sessions are organized into workspaces - Each user has a default "General" workspace - Custom colors and icons for visual organization - Workspace-specific session isolation ### Chat Flow 1. First message in a session is the system message (prompt) 2. User sends message → Handler validates → Service routes to appropriate LLM provider 3. LLM streams response → Server-Sent Events (SSE) → Frontend renders incrementally 4. Messages stored in PostgreSQL with full history ### File Uploads - Text files supported for all models - Multimedia files require model support (GPT-4 Vision, Claude 3+, Gemini) - Files associated with messages via `chat_file` table ### Snapshots - Create shareable static pages from conversations (like ShareGPT) - Full-text search support (English) for organizing conversation history - Can continue conversations from snapshots ### Prompt Management - Built-in prompt templates stored in `chat_prompt` table - Quick access via '/' shortcut in chat interface ## Testing ### Running Backend Tests ```bash cd api go test ./... ``` ### Running Frontend Tests ```bash cd web npm test ``` ### Running E2E Tests ```bash cd e2e export DATABASE_URL=postgres://... npx playwright test ``` ## Adding a New LLM Model See documentation: `docs/add_model_en.md` and `docs/add_model_zh.md` **Summary**: 1. Add model configuration to `chat_model` table (via admin UI or SQL) 2. Implement provider in `api/llm//` if new provider type 3. Update routing logic in `chat_main_service.go` if needed 4. Set appropriate `api_type` field: `openai`, `claude`, `gemini`, `ollama`, or `custom` ## Common Gotchas 1. **SQLC Code Generation**: After modifying `schema.sql` or query files, always run `sqlc generate` from the `api/` directory 2. **Hot Reload**: Both frontend (Rsbuild) and backend (Air) auto-reload on code changes 3. **Database Migrations**: Schema changes are handled via `ALTER TABLE IF NOT EXISTS` statements in `schema.sql` 4. **Rate Limiting**: Applies globally (100/10min) unless per-model rate limiting is enabled 5. **Model API Types**: The `api_type` column determines which LLM provider client is used 6. **Session Context**: By default, only the latest 4 messages are sent to the LLM (plus system prompt) 7. **Title Generation**: Conversation titles are optionally generated by `gemini-2.0-flash`; if not configured, uses first 100 chars of prompt ## Documentation - Local development: `docs/dev_locally_en.md`, `docs/dev_locally_zh.md` - Deployment: `docs/deployment_en.md`, `docs/deployment_zh.md` - Ollama integration: `docs/ollama_en.md`, `docs/ollama_zh.md` - Snapshots vs ChatBots: `docs/snapshots_vs_chatbots_en.md` - Adding models: `docs/add_model_en.md` - Dev documentation: `docs/dev/` (VFS, error handling, integration guides) ## Technology Stack Summary **Frontend**: Vue 3, Pinia, Naive UI, Rsbuild, Tailwind CSS, TypeScript **Backend**: Go, Gorilla Mux, SQLC, PostgreSQL, Air (hot reload) **Testing**: Playwright (E2E), Vitest (frontend unit tests) **LLM SDKs**: Custom HTTP clients for each provider ================================================ FILE: Dockerfile ================================================ FROM node:16 as frontend_builder # Set the working directory to /app WORKDIR /app # Copy the package.json and package-lock.json files to the container COPY web/package*.json ./ # Install dependencies RUN npm install # Copy the remaining application files to the container COPY web/ . # Build the application RUN npm run build FROM golang:1.24-alpine3.20 AS builder WORKDIR /app COPY api/go.mod api/go.sum ./ RUN go mod download COPY api/ . # cp -rf /app/dist/* /app/static/ COPY --from=frontend_builder /app/dist/ ./static/ RUN CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -a -installsuffix cgo -o /app/app FROM alpine:3.20 WORKDIR /app COPY --from=builder /app/app /app # for go timezone work COPY --from=builder /usr/local/go/lib/time/zoneinfo.zip /app/zoneinfo.zip ENV ZONEINFO=/app/zoneinfo.zip EXPOSE 8080 ENTRYPOINT ["/app/app"] ================================================ FILE: README.md ================================================ ## Demo image image thinking ![image](https://github.com/user-attachments/assets/ad38194e-dd13-4eb0-b946-81c29a37955d) image Screenshot 2025-09-11 at 8 05 03 PM ![image](https://github.com/user-attachments/assets/5b3751e4-eaa1-4a79-b47a-9b073c63eb04) image chat records chat record comments ## 规则 - 第一个消息是系统消息(prompt) - 上下文默认附带最新创建的4条消息 - 第一个注册的用户是管理员 - 默认限流 100 chatGPT call /10分钟 (OPENAI_RATELIMIT=100) - 根据对话生成可以分享的静态页面(like ShareGPT), 也可以继续会话. - 对话快照目录(对话集), 支持全文查找(English), 方便整理, 搜索会话记录. - 支持OPEN AI, Claude 模型 - 支持Ollama host模型, 配置参考: https://github.com/swuecho/chat/discussions/396 - 支持上传文本文件 - 支持多媒体文件, 需要模型支持 - 提示词管理, 提示词快捷键 '/' > (可选)对话标题用 `gemini-2.0-flash` 生成, 所以需要配置该模型, 不配置默认用提示词前100个字符 ## 文档 - [添加新模型指南](https://github.com/swuecho/chat/blob/master/docs/add_model_zh.md) - [快照 vs 聊天机器人](https://github.com/swuecho/chat/blob/master/docs/snapshots_vs_chatbots_zh.md) - [使用本地Ollama](https://github.com/swuecho/chat/blob/master/docs/ollama_zh.md) - [论坛](https://github.com/swuecho/chat/discussions) ## 开发指南 - [本地开发指南](https://github.com/swuecho/chat/blob/master/docs/dev_locally_zh.md) ## 部署指南 - [部署指南](https://github.com/swuecho/chat/blob/master/docs/deployment_zh.md) ## 致谢 - web: [ChatGPT-Web](https://github.com/Chanzhaoyu/chatgpt-web) 复制过来的 。 - api : 参考 [Kerwin1202](https://github.com/Kerwin1202)'s [Chanzhaoyu/chatgpt-web#589](https://github.com/Chanzhaoyu/chatgpt-web/pull/589) 的node版本在chatgpt帮助下写的 ## LICENCE: MIT ## Rules - The first message is a system message (prompt) - By default, the latest 4 messages are included in context - The first registered user becomes administrator - Default rate limit: 100 ChatGPT calls per 10 minutes (OPENAI_RATELIMIT=100) - Generate shareable static pages from conversations (like ShareGPT), or continue conversations - Conversation snapshots directory supports full-text search (English), making it easy to organize and search conversation history - Supports OpenAI and Claude models - Supports Ollama host models, configuration reference: https://github.com/swuecho/chat/discussions/396 - Supports text file uploads - Supports multimedia files (requires model support) - Prompt management with '/' shortcut > (Optional) Conversation titles are generated by `gemini-2.0-flash`, so this model needs to be configured. If not configured, the first 100 characters of the prompt will be used as the title. ## Documentation - [Adding New Models Guide](https://github.com/swuecho/chat/blob/master/docs/add_model_en.md) - [Snapshots vs ChatBots](https://github.com/swuecho/chat/blob/master/docs/snapshots_vs_chatbots_en.md) - [Using Local Ollama](https://github.com/swuecho/chat/blob/master/docs/ollama_en.md) - [Community Discussions](https://github.com/swuecho/chat/discussions) ## Development Guide - [Local Development Guide](https://github.com/swuecho/chat/blob/master/docs/dev_locally_en.md) ## Deployment Guide - [Deployment Guide](https://github.com/swuecho/chat/blob/master/docs/deployment_en.md) ## Acknowledgments - web: copied from chatgpt-web - api: based on the node version of [Kerwin1202](https://github.com/Kerwin1202)'s [Chanzhaoyu/chatgpt-web#589](https://github.com/Chanzhaoyu/chatgpt-web/pull/589) and written with the help of chatgpt. ================================================ FILE: api/.air.toml ================================================ root = "." testdata_dir = "testdata" tmp_dir = "tmp" [build] args_bin = [] #bin = "./tmp/main" #cmd = "go build -o ./tmp/main ." delay = 0 exclude_dir = ["assets", "tmp", "vendor", "testdata"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false follow_symlink = false full_bin = "" include_dir = [] include_ext = ["go", "tpl", "tmpl", "html"] include_file = [] kill_delay = "0s" log = "build-errors.log" rerun = false rerun_delay = 500 send_interrupt = false stop_on_error = false [color] app = "" build = "yellow" main = "magenta" runner = "green" watcher = "cyan" [log] main_only = false time = false [misc] clean_on_exit = false [screen] clear_on_rebuild = false keep_scroll = true ================================================ FILE: api/.github/workflows/go.yml ================================================ # This workflow will build a golang project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go name: Go on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v3 with: go-version: 1.24 - name: Build run: go build -v ./... - name: Test run: go test -v ./... # build docker image base on Dockerfile ================================================ FILE: api/.gitignore ================================================ tmp/ chat_backend env.sh static/static ================================================ FILE: api/.vscode/settings.json ================================================ { "editor.fontFamily": "Go Mono", "go.useLanguageServer": true, "files.watcherExclude": { "**/target": true } } ================================================ FILE: api/LICENSE ================================================ MIT License Copyright (c) 2023-2024 Hao Wu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: api/Makefile ================================================ .DEFAULT_GOAL:=build fmt: go fmt ./... .PHONY: fmt lint: fmt golint ./... .PHONY: lint vet: fmt go vet ./... .PHONY: vet build: vet go build .PHONY: build install: go install github.com/air-verse/air@latest go mod tidy serve: @echo "Starting server..." echo 'sudo lsof -i -P -n | grep 8080' echo $(OPENAI_API_KEY) echo $(PG_HOST) air ================================================ FILE: api/README.md ================================================ # architecture request -> mux(router) -> sql generated code -> database -> sql ## library used 1. sqlc to connect go code to sql (sql is mostly generated by chatgpt) 2. mux as router ## ChatGPT version When it comes to building web applications with Go, there are several choices for libraries and frameworks. One of the most popular options for creating a web server is mux, which provides a flexible and powerful routing system. In addition to handling incoming requests and routing them to the appropriate handler, a web application will typically need to interact with a database. This is where sqlc comes in, a Go library that helps connect your Go code to your database using SQL. Using these two tools together, you can quickly create a powerful and efficient web application that uses SQL for data storage and retrieval. Here's how it all fits together. When a request comes into your application, it's handled by mux. Mux inspects the incoming request and routes it to the appropriate function or handler in your Go code. Your Go code, in turn, uses sqlc generated go code (based on SQL) that interacts with your database. This generated go code is used to fetch data, store new data, and update existing data. The SQL code is compiled and executed by your database, and the result is returned to your Go code. Overall, this architecture provides a clean and modular approach to building web applications with Go. By leveraging powerful libraries like mux and sqlc, you can focus on writing application logic rather than worrying about the low-level details of routing and database access. In summary, if you're building a web application with Go, you should definitely consider using mux as your router and sqlc to connect your Go code to your database. The combination of these two libraries makes it easy to build scalable and reliable web applications that are both easy to maintain and performant. ================================================ FILE: api/admin_handler.go ================================================ package main import ( "encoding/json" "net/http" "strconv" "github.com/gorilla/mux" "github.com/swuecho/chat_backend/sqlc_queries" ) type AdminHandler struct { service *AuthUserService } func NewAdminHandler(service *AuthUserService) *AdminHandler { return &AdminHandler{ service: service, } } func (h *AdminHandler) RegisterRoutes(router *mux.Router) { // admin routes (without /admin prefix since router already handles it) router.HandleFunc("/users", h.CreateUser).Methods(http.MethodPost) router.HandleFunc("/users", h.UpdateUser).Methods(http.MethodPut) router.HandleFunc("/rate_limit", h.UpdateRateLimit).Methods(http.MethodPost) router.HandleFunc("/user_stats", h.UserStatHandler).Methods(http.MethodPost) router.HandleFunc("/user_analysis/{email}", h.UserAnalysisHandler).Methods(http.MethodGet) router.HandleFunc("/user_session_history/{email}", h.UserSessionHistoryHandler).Methods(http.MethodGet) router.HandleFunc("/session_messages/{sessionUuid}", h.SessionMessagesHandler).Methods(http.MethodGet) } func (h *AdminHandler) CreateUser(w http.ResponseWriter, r *http.Request) { var userParams sqlc_queries.CreateAuthUserParams err := json.NewDecoder(r.Body).Decode(&userParams) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } user, err := h.service.CreateAuthUser(r.Context(), userParams) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to create user")) return } json.NewEncoder(w).Encode(user) } func (h *AdminHandler) UpdateUser(w http.ResponseWriter, r *http.Request) { var userParams sqlc_queries.UpdateAuthUserByEmailParams err := json.NewDecoder(r.Body).Decode(&userParams) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } user, err := h.service.q.UpdateAuthUserByEmail(r.Context(), userParams) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update user")) return } json.NewEncoder(w).Encode(user) } func (h *AdminHandler) UserStatHandler(w http.ResponseWriter, r *http.Request) { var pagination Pagination err := json.NewDecoder(r.Body).Decode(&pagination) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } userStatsRows, total, err := h.service.GetUserStats(r.Context(), pagination, int32(appConfig.OPENAI.RATELIMIT)) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get user stats")) return } // Create a new []interface{} slice with same length as userStatsRows data := make([]interface{}, len(userStatsRows)) // Copy the contents of userStatsRows into data for i, v := range userStatsRows { divider := v.TotalChatMessages3Days var avg int64 if divider > 0 { avg = v.TotalTokenCount3Days / v.TotalChatMessages3Days } else { avg = 0 } data[i] = UserStat{ Email: v.UserEmail, FirstName: v.FirstName, LastName: v.LastName, TotalChatMessages: v.TotalChatMessages, TotalChatMessages3Days: v.TotalChatMessages3Days, RateLimit: v.RateLimit, TotalChatMessagesTokenCount: v.TotalTokenCount, TotalChatMessages3DaysTokenCount: v.TotalTokenCount3Days, AvgChatMessages3DaysTokenCount: avg, } } json.NewEncoder(w).Encode(Pagination{ Page: pagination.Page, Size: pagination.Size, Total: total, Data: data, }) } func (h *AdminHandler) UpdateRateLimit(w http.ResponseWriter, r *http.Request) { var rateLimitRequest RateLimitRequest err := json.NewDecoder(r.Body).Decode(&rateLimitRequest) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } rate, err := h.service.q.UpdateAuthUserRateLimitByEmail(r.Context(), sqlc_queries.UpdateAuthUserRateLimitByEmailParams{ Email: rateLimitRequest.Email, RateLimit: rateLimitRequest.RateLimit, }) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update rate limit")) return } json.NewEncoder(w).Encode( map[string]int32{ "rate": rate, }) } func (h *AdminHandler) UserAnalysisHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) email := vars["email"] if email == "" { RespondWithAPIError(w, ErrValidationInvalidInput("Email parameter is required")) return } analysisData, err := h.service.GetUserAnalysis(r.Context(), email, int32(appConfig.OPENAI.RATELIMIT)) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get user analysis")) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(analysisData) } type SessionHistoryResponse struct { Data []SessionHistoryInfo `json:"data"` Total int64 `json:"total"` Page int32 `json:"page"` Size int32 `json:"size"` } func (h *AdminHandler) UserSessionHistoryHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) email := vars["email"] if email == "" { RespondWithAPIError(w, ErrValidationInvalidInput("Email parameter is required")) return } // Parse pagination parameters pageStr := r.URL.Query().Get("page") sizeStr := r.URL.Query().Get("size") page := int32(1) size := int32(10) if pageStr != "" { if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { page = int32(p) } } if sizeStr != "" { if s, err := strconv.Atoi(sizeStr); err == nil && s > 0 && s <= 100 { size = int32(s) } } sessionHistory, total, err := h.service.GetUserSessionHistory(r.Context(), email, page, size) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get user session history")) return } response := SessionHistoryResponse{ Data: sessionHistory, Total: total, Page: page, Size: size, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func (h *AdminHandler) SessionMessagesHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) sessionUuid := vars["sessionUuid"] if sessionUuid == "" { RespondWithAPIError(w, ErrValidationInvalidInput("Session UUID parameter is required")) return } messages, err := h.service.q.GetChatMessagesBySessionUUIDForAdmin(r.Context(), sessionUuid) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get session messages")) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(messages) } ================================================ FILE: api/ai/model.go ================================================ package ai import ( "encoding/json" "fmt" ) type Role int const ( System Role = iota User Assistant ) func (r Role) String() string { switch r { case System: return "system" case User: return "user" case Assistant: return "assistant" default: return "" } } func StringToRole(s string) (Role, error) { switch s { case "system": return System, nil case "user": return User, nil case "assistant": return Assistant, nil default: return 0, fmt.Errorf("invalid role string: %s", s) } } func (r *Role) UnmarshalJSON(data []byte) error { var roleStr string err := json.Unmarshal(data, &roleStr) if err != nil { return err } switch roleStr { case "system": *r = System case "user": *r = User case "assistant": *r = Assistant default: return fmt.Errorf("invalid role string: %s", roleStr) } return nil } func (r Role) MarshalJSON() ([]byte, error) { switch r { case System: return json.Marshal("system") case User: return json.Marshal("user") case Assistant: return json.Marshal("assistant") default: return nil, fmt.Errorf("invalid role value: %d", r) } } ================================================ FILE: api/artifact_instruction.txt ================================================ ARTIFACT CREATION GUIDELINES - MANDATORY COMPLIANCE REQUIRED: ⚠️ CRITICAL: These formatting rules are REQUIRED for artifact rendering. Deviation will cause display failures. ## MANDATORY ARTIFACT FORMATS (EXACT SYNTAX REQUIRED): ### HTML Applications: ```html [Complete HTML content with inline CSS and JavaScript(Preact)] ``` ### SVG Graphics: ```svg [Complete SVG markup] ``` ### Mermaid Diagrams: ```mermaid [Mermaid diagram syntax] ``` ### JSON Data: ```json [Valid JSON data] ``` ### Executable Code: ```javascript [JavaScript/TypeScript code] ``` ```python [Python code] ``` ## FORMATTING COMPLIANCE CHECKLIST: ✅ Comment MUST be on the SAME LINE as opening ``` ✅ Use EXACT format: `` or `` ✅ Include descriptive, specific title explaining functionality ✅ No extra spaces or characters in comment syntax ✅ Complete, self-contained code within blocks ❌ COMMON ERRORS TO AVOID: - Comment on separate line from ``` - Missing or incorrect comment format - Generic titles like "Code" or "Example" - Incomplete or broken code - External dependencies in HTML artifacts ## WHEN TO CREATE ARTIFACTS: ### ALWAYS create artifacts for: - Interactive web applications, forms, games, tools - Data visualizations, charts, graphs, dashboards - Diagrams, flowcharts, visual representations - Working code examples demonstrating functionality - Calculators, converters, utility applications - Rich data displays or formatted outputs - Any content meant to be rendered/executed ### NEVER create artifacts for: - Simple text responses - Code snippets for reference only - Incomplete or pseudo-code - Content requiring external files ## HTML ARTIFACT STANDARDS: ### REQUIRED STRUCTURE: ```html [App Title] ``` ### TECHNICAL REQUIREMENTS: - Use Preact with HTM: `import { html, render } from 'https://unpkg.com/htm/preact/standalone.module.js'` - Modern ES6+ syntax only - Responsive design with proper viewport meta - Semantic HTML5 elements - Accessible UI with proper ARIA labels - Error handling for user interactions ## EXECUTABLE CODE STANDARDS: ### JavaScript/TypeScript FEATURES: - Output: console.log(), console.error(), console.warn() - Graphics: createCanvas(width, height) for visualizations - Libraries: `// @import libraryName` (lodash, d3, chart.js, moment, axios, rxjs, p5, three, fabric) - Return values automatically displayed - Built-in timeout and resource monitoring ### Python FEATURES: - Output: print() for all results (auto-captured) - Plotting: matplotlib plots auto-displayed as PNG - Libraries: numpy, pandas, matplotlib, scipy, scikit-learn, requests, seaborn, plotly - Memory and execution monitoring included - No file/network access (sandboxed) ## QUALITY ASSURANCE: ### PRE-SUBMISSION CHECKLIST: 1. ✅ Verify exact comment syntax on same line as ``` 2. ✅ Test all interactive functionality 3. ✅ Ensure complete self-contained code 4. ✅ Validate responsive design (HTML) 5. ✅ Confirm proper error handling 6. ✅ Check accessibility features 7. ✅ Verify cross-browser compatibility ### ARTIFACT TITLE GUIDELINES: - Be specific and descriptive - Include primary function/purpose - Avoid generic terms - Examples: - ✅ "Interactive BMI Calculator with Health Recommendations" - ✅ "Real-time Stock Price Chart with Technical Indicators" - ❌ "Calculator" - ❌ "Chart" ## RENDERER BEHAVIOR: The artifact viewer uses specialized renderers: - **HTML**: Full browser environment with Preact support - **SVG**: Native SVG rendering with interactive capabilities - **Mermaid**: Diagram engine with theme support - **JSON**: Formatted tree view with syntax highlighting - **JavaScript**: Node.js-like environment with canvas support - **Python**: Scientific computing sandbox with plot display ⚠️ FINAL REMINDER: Artifacts that don't follow these exact formatting rules will fail to render. Always double-check syntax before submission. ================================================ FILE: api/auth/auth.go ================================================ package auth import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "fmt" "strings" "github.com/rotisserie/eris" "golang.org/x/crypto/pbkdf2" ) const ( iterations = 260000 saltSize = 16 keySize = 32 ) func generateSalt() ([]byte, error) { salt := make([]byte, saltSize) _, err := rand.Read(salt) return salt, err } func GeneratePasswordHash(password string) (string, error) { salt, err := generateSalt() if err != nil { return "", eris.Wrap(err, "error generating salt: ") } hash := pbkdf2.Key([]byte(password), salt, iterations, keySize, sha256.New) encodedHash := base64.StdEncoding.EncodeToString(hash) encodedSalt := base64.StdEncoding.EncodeToString(salt) passwordHash := fmt.Sprintf("pbkdf2_sha256$%d$%s$%s", iterations, encodedSalt, encodedHash) return passwordHash, nil } func ValidatePassword(password, hash string) bool { fields := strings.Split(hash, "$") if len(fields) != 4 || fields[0] != "pbkdf2_sha256" || fields[1] != fmt.Sprintf("%d", iterations) { return false } encodedSalt := fields[2] decodedSalt, err := base64.StdEncoding.DecodeString(encodedSalt) if err != nil { return false } encodedHash := fields[3] decodedHash, err := base64.StdEncoding.DecodeString(encodedHash) if err != nil { return false } computedHash := pbkdf2.Key([]byte(password), decodedSalt, iterations, keySize, sha256.New) return subtle.ConstantTimeCompare(decodedHash, computedHash) == 1 } func GenerateRandomPassword() (string, error) { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" password := make([]byte, 12) _, err := rand.Read(password) if err != nil { return "", eris.Wrap(err, "failed to generate random password") } for i := 0; i < len(password); i++ { password[i] = letters[int(password[i])%len(letters)] } return string(password), nil } ================================================ FILE: api/auth/auth_test.go ================================================ package auth import ( "fmt" "strings" "testing" ) func TestGeneratePasswordHash(t *testing.T) { password := "mypassword" hash, err := GeneratePasswordHash(password) if err != nil { t.Fatalf("error generating password hash: %v", err) } fmt.Println(hash) // Check that the hash has the correct format fields := strings.Split(hash, "$") if len(fields) != 4 || fields[0] != "pbkdf2_sha256" || fields[1] != "260000" { t.Errorf("unexpected hash format: %s", hash) } // Check that we can successfully validate the password using the hash valid := ValidatePassword(password, hash) if !valid { t.Error("generated hash does not validate password") } } func TestGeneratePasswordHash2(t *testing.T) { password := "@WuHao5" hash, err := GeneratePasswordHash(password) if err != nil { t.Fatalf("error generating password hash: %v", err) } fmt.Println(hash) // Check that the hash has the correct format fields := strings.Split(hash, "$") if len(fields) != 4 || fields[0] != "pbkdf2_sha256" || fields[1] != "260000" { t.Errorf("unexpected hash format: %s", hash) } // Check that we can successfully validate the password using the hash valid := ValidatePassword(password, hash) if !valid { t.Error("generated hash does not validate password") } } func TestPass(t *testing.T) { hash := "pbkdf2_sha256$260000$TSefBGfPi5fY+4whotY5sQ==$/1CeWE2PG6aYdW2DSxYyVol+HEZBmAfDj7zMgEMlxgg=" password := "using555" // Check that we can successfully validate the password using the hash valid := ValidatePassword(password, hash) if !valid { t.Error("generated hash does not validate password") } } ================================================ FILE: api/auth/token.go ================================================ package auth import ( "encoding/base64" "errors" "fmt" "math/rand" "net/http" "strconv" "time" jwt "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) func NewUUID() string { uuidv7, err := uuid.NewV7() if err != nil { return uuid.NewString() } return uuidv7.String() } var ErrInvalidToken = errors.New("invalid token") const ( TokenTypeAccess = "access" TokenTypeRefresh = "refresh" ) func GenJwtSecretAndAudience() (string, string) { // Generate a random byte string to use as the secret secretBytes := make([]byte, 32) rand.Read(secretBytes) // Convert the byte string to a base64 encoded string secret := base64.StdEncoding.EncodeToString(secretBytes) // Generate a random string to use as the audience const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" audienceBytes := make([]byte, 32) for i := range audienceBytes { audienceBytes[i] = letters[rand.Intn(len(letters))] } audience := string(audienceBytes) return secret, audience } func GenerateToken(userID int32, role string, secret, jwt_audience string, lifetime time.Duration, tokenType string) (string, error) { if tokenType == "" { tokenType = TokenTypeAccess } expires := time.Now().Add(lifetime).Unix() notBefore := time.Now().Unix() issuer := "https://www.bestqa.net" claims := jwt.MapClaims{ "user_id": strconv.FormatInt(int64(userID), 10), "exp": expires, "role": role, "jti": NewUUID(), "iss": issuer, "nbf": notBefore, "aud": jwt_audience, "token_type": tokenType, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token.Header["kid"] = "dfsafdsafdsafadsfdasdfs" signedToken, err := token.SignedString([]byte(secret)) if err != nil { return "", err } return signedToken, nil } func ValidateToken(tokenString string, secret string, expectedTokenType string) (int32, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Verify the signing algorithm and secret key used to sign the token if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token) } return []byte(secret), nil }) if err != nil { return 0, ErrInvalidToken } claims, ok := token.Claims.(jwt.MapClaims) if !ok || !token.Valid { return 0, ErrInvalidToken } userIDStr, ok := claims["user_id"].(string) if !ok { return 0, ErrInvalidToken } tokenType, ok := claims["token_type"].(string) if !ok { // Support legacy tokens that were minted without token_type; treat them as access tokens // so existing forever tokens continue to work. if expectedTokenType == "" || expectedTokenType == TokenTypeAccess { tokenType = TokenTypeAccess } else { return 0, ErrInvalidToken } } if expectedTokenType != "" && tokenType != expectedTokenType { return 0, ErrInvalidToken } i, err := strconv.Atoi(userIDStr) if err != nil { return -1, err } return int32(i), nil } func GetExpireSecureCookie(value string, isHttps bool) *http.Cookie { utcOffset := time.Now().UTC().Add(-24 * time.Hour) options := &http.Cookie{ Name: "jwt", Value: value, Path: "/", HttpOnly: true, Secure: isHttps, SameSite: http.SameSiteStrictMode, Expires: utcOffset, } return options } ================================================ FILE: api/auth/token_test.go ================================================ package auth import ( "fmt" "testing" "time" ) func TestGenerateToken(t *testing.T) { user_id := int32(0) secret := "abedefg" lifetime := 8 * time.Hour token, err := GenerateToken(user_id, "user", secret, "aud", lifetime, TokenTypeAccess) if err != nil { t.Fatalf("error generating password hash: %v", err) } // Check that the hash has the correct format // Check that we can successfully validate the password using the hash fmt.Println(token) user_id_after_valid, err := ValidateToken(token, secret, TokenTypeAccess) if err != nil { t.Error("generated token does not validate ") } if user_id != user_id_after_valid { t.Error("generated token does not validate ") } } ================================================ FILE: api/bot_answer_history_handler.go ================================================ package main import ( "encoding/json" "net/http" "strconv" "github.com/gorilla/mux" "github.com/swuecho/chat_backend/sqlc_queries" ) type BotAnswerHistoryHandler struct { service *BotAnswerHistoryService } func NewBotAnswerHistoryHandler(q *sqlc_queries.Queries) *BotAnswerHistoryHandler { service := NewBotAnswerHistoryService(q) return &BotAnswerHistoryHandler{service: service} } func (h *BotAnswerHistoryHandler) Register(router *mux.Router) { router.HandleFunc("/bot_answer_history", h.CreateBotAnswerHistory).Methods(http.MethodPost) router.HandleFunc("/bot_answer_history/{id}", h.GetBotAnswerHistoryByID).Methods(http.MethodGet) router.HandleFunc("/bot_answer_history/bot/{bot_uuid}", h.GetBotAnswerHistoryByBotUUID).Methods(http.MethodGet) router.HandleFunc("/bot_answer_history/user/{user_id}", h.GetBotAnswerHistoryByUserID).Methods(http.MethodGet) router.HandleFunc("/bot_answer_history/{id}", h.UpdateBotAnswerHistory).Methods(http.MethodPut) router.HandleFunc("/bot_answer_history/{id}", h.DeleteBotAnswerHistory).Methods(http.MethodDelete) router.HandleFunc("/bot_answer_history/bot/{bot_uuid}/count", h.GetBotAnswerHistoryCountByBotUUID).Methods(http.MethodGet) router.HandleFunc("/bot_answer_history/user/{user_id}/count", h.GetBotAnswerHistoryCountByUserID).Methods(http.MethodGet) router.HandleFunc("/bot_answer_history/bot/{bot_uuid}/latest", h.GetLatestBotAnswerHistoryByBotUUID).Methods(http.MethodGet) } func (h *BotAnswerHistoryHandler) CreateBotAnswerHistory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithDebugInfo(err.Error())) return } var params sqlc_queries.CreateBotAnswerHistoryParams if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Invalid request body").WithDebugInfo(err.Error())) return } // Set the user ID from context params.UserID = userID history, err := h.service.CreateBotAnswerHistory(ctx, params) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to create bot answer history")) return } RespondWithJSON(w, http.StatusCreated, history) } func (h *BotAnswerHistoryHandler) GetBotAnswerHistoryByID(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] if id == "" { RespondWithAPIError(w, ErrValidationInvalidInput("ID is required")) return } idInt, err := strconv.ParseInt(id, 10, 32) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Invalid ID format")) return } history, err := h.service.GetBotAnswerHistoryByID(r.Context(), int32(idInt)) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get bot answer history")) return } RespondWithJSON(w, http.StatusOK, history) } func (h *BotAnswerHistoryHandler) GetBotAnswerHistoryByBotUUID(w http.ResponseWriter, r *http.Request) { botUUID := mux.Vars(r)["bot_uuid"] if botUUID == "" { RespondWithAPIError(w, ErrValidationInvalidInput("Bot UUID is required")) return } limit, offset := getPaginationParams(r) history, err := h.service.GetBotAnswerHistoryByBotUUID(r.Context(), botUUID, limit, offset) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get bot answer history")) return } // Get total count for pagination totalCount, err := h.service.GetBotAnswerHistoryCountByBotUUID(r.Context(), botUUID) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get bot answer history count")) return } // Calculate total pages totalPages := totalCount / int64(limit) if totalCount%int64(limit) > 0 { totalPages++ } // Return paginated response RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "items": history, "totalPages": totalPages, "totalCount": totalCount, }) } func (h *BotAnswerHistoryHandler) GetBotAnswerHistoryByUserID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithDebugInfo(err.Error())) return } limit, offset := getPaginationParams(r) history, err := h.service.GetBotAnswerHistoryByUserID(ctx, userID, limit, offset) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get bot answer history")) return } RespondWithJSON(w, http.StatusOK, history) } func (h *BotAnswerHistoryHandler) UpdateBotAnswerHistory(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] if id == "" { RespondWithAPIError(w, ErrValidationInvalidInput("ID is required")) return } var params sqlc_queries.UpdateBotAnswerHistoryParams if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Invalid request body").WithDebugInfo(err.Error())) return } history, err := h.service.UpdateBotAnswerHistory(r.Context(), params.ID, params.Answer, params.TokensUsed) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to update bot answer history")) return } RespondWithJSON(w, http.StatusOK, history) } func (h *BotAnswerHistoryHandler) DeleteBotAnswerHistory(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] if id == "" { RespondWithAPIError(w, ErrValidationInvalidInput("ID is required")) return } idInt, err := strconv.ParseInt(id, 10, 32) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Invalid ID format")) return } if err := h.service.DeleteBotAnswerHistory(r.Context(), int32(idInt)); err != nil { RespondWithAPIError(w, WrapError(err, "Failed to delete bot answer history")) return } w.WriteHeader(http.StatusNoContent) } func (h *BotAnswerHistoryHandler) GetBotAnswerHistoryCountByBotUUID(w http.ResponseWriter, r *http.Request) { botUUID := mux.Vars(r)["bot_uuid"] if botUUID == "" { RespondWithAPIError(w, ErrValidationInvalidInput("Bot UUID is required")) return } count, err := h.service.GetBotAnswerHistoryCountByBotUUID(r.Context(), botUUID) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get bot answer history count")) return } RespondWithJSON(w, http.StatusOK, map[string]int64{"count": count}) } func (h *BotAnswerHistoryHandler) GetBotAnswerHistoryCountByUserID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithDebugInfo(err.Error())) return } count, err := h.service.GetBotAnswerHistoryCountByUserID(ctx, userID) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get bot answer history count")) return } RespondWithJSON(w, http.StatusOK, map[string]int64{"count": count}) } func (h *BotAnswerHistoryHandler) GetLatestBotAnswerHistoryByBotUUID(w http.ResponseWriter, r *http.Request) { botUUID := mux.Vars(r)["bot_uuid"] if botUUID == "" { RespondWithAPIError(w, ErrValidationInvalidInput("Bot UUID is required")) return } limit := getLimitParam(r, 1) history, err := h.service.GetLatestBotAnswerHistoryByBotUUID(r.Context(), botUUID, limit) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to get latest bot answer history")) return } RespondWithJSON(w, http.StatusOK, history) } ================================================ FILE: api/bot_answer_history_service.go ================================================ package main import ( "context" "github.com/rotisserie/eris" "github.com/swuecho/chat_backend/sqlc_queries" ) type BotAnswerHistoryService struct { q *sqlc_queries.Queries } // NewBotAnswerHistoryService creates a new BotAnswerHistoryService func NewBotAnswerHistoryService(q *sqlc_queries.Queries) *BotAnswerHistoryService { return &BotAnswerHistoryService{q: q} } // CreateBotAnswerHistory creates a new bot answer history entry func (s *BotAnswerHistoryService) CreateBotAnswerHistory(ctx context.Context, params sqlc_queries.CreateBotAnswerHistoryParams) (sqlc_queries.BotAnswerHistory, error) { history, err := s.q.CreateBotAnswerHistory(ctx, params) if err != nil { return sqlc_queries.BotAnswerHistory{}, eris.Wrap(err, "failed to create bot answer history") } return history, nil } // GetBotAnswerHistoryByID gets a bot answer history entry by ID func (s *BotAnswerHistoryService) GetBotAnswerHistoryByID(ctx context.Context, id int32) (sqlc_queries.GetBotAnswerHistoryByIDRow, error) { history, err := s.q.GetBotAnswerHistoryByID(ctx, id) if err != nil { return sqlc_queries.GetBotAnswerHistoryByIDRow{}, eris.Wrap(err, "failed to get bot answer history by ID") } return history, nil } // GetBotAnswerHistoryByBotUUID gets paginated bot answer history for a specific bot func (s *BotAnswerHistoryService) GetBotAnswerHistoryByBotUUID(ctx context.Context, botUUID string, limit, offset int32) ([]sqlc_queries.GetBotAnswerHistoryByBotUUIDRow, error) { params := sqlc_queries.GetBotAnswerHistoryByBotUUIDParams{ BotUuid: botUUID, Limit: limit, Offset: offset, } history, err := s.q.GetBotAnswerHistoryByBotUUID(ctx, params) if err != nil { return nil, eris.Wrap(err, "failed to get bot answer history by bot UUID") } return history, nil } // GetBotAnswerHistoryByUserID gets paginated bot answer history for a specific user func (s *BotAnswerHistoryService) GetBotAnswerHistoryByUserID(ctx context.Context, userID, limit, offset int32) ([]sqlc_queries.GetBotAnswerHistoryByUserIDRow, error) { params := sqlc_queries.GetBotAnswerHistoryByUserIDParams{ UserID: userID, Limit: limit, Offset: offset, } history, err := s.q.GetBotAnswerHistoryByUserID(ctx, params) if err != nil { return nil, eris.Wrap(err, "failed to get bot answer history by user ID") } return history, nil } // UpdateBotAnswerHistory updates an existing bot answer history entry func (s *BotAnswerHistoryService) UpdateBotAnswerHistory(ctx context.Context, id int32, answer string, tokensUsed int32) (sqlc_queries.BotAnswerHistory, error) { params := sqlc_queries.UpdateBotAnswerHistoryParams{ ID: id, Answer: answer, TokensUsed: tokensUsed, } history, err := s.q.UpdateBotAnswerHistory(ctx, params) if err != nil { return sqlc_queries.BotAnswerHistory{}, eris.Wrap(err, "failed to update bot answer history") } return history, nil } // DeleteBotAnswerHistory deletes a bot answer history entry by ID func (s *BotAnswerHistoryService) DeleteBotAnswerHistory(ctx context.Context, id int32) error { err := s.q.DeleteBotAnswerHistory(ctx, id) if err != nil { return eris.Wrap(err, "failed to delete bot answer history") } return nil } // GetBotAnswerHistoryCountByBotUUID gets the count of history entries for a bot func (s *BotAnswerHistoryService) GetBotAnswerHistoryCountByBotUUID(ctx context.Context, botUUID string) (int64, error) { count, err := s.q.GetBotAnswerHistoryCountByBotUUID(ctx, botUUID) if err != nil { return 0, eris.Wrap(err, "failed to get bot answer history count by bot UUID") } return count, nil } // GetBotAnswerHistoryCountByUserID gets the count of history entries for a user func (s *BotAnswerHistoryService) GetBotAnswerHistoryCountByUserID(ctx context.Context, userID int32) (int64, error) { count, err := s.q.GetBotAnswerHistoryCountByUserID(ctx, userID) if err != nil { return 0, eris.Wrap(err, "failed to get bot answer history count by user ID") } return count, nil } // GetLatestBotAnswerHistoryByBotUUID gets the latest history entries for a bot func (s *BotAnswerHistoryService) GetLatestBotAnswerHistoryByBotUUID(ctx context.Context, botUUID string, limit int32) ([]sqlc_queries.GetLatestBotAnswerHistoryByBotUUIDRow, error) { params := sqlc_queries.GetLatestBotAnswerHistoryByBotUUIDParams{ BotUuid: botUUID, Limit: limit, } history, err := s.q.GetLatestBotAnswerHistoryByBotUUID(ctx, params) if err != nil { return nil, eris.Wrap(err, "failed to get latest bot answer history by bot UUID") } return history, nil } ================================================ FILE: api/chat_artifact.go ================================================ package main import ( "regexp" "strings" ) // extractArtifacts detects and extracts artifacts from message content func extractArtifacts(content string) []Artifact { var artifacts []Artifact // Pattern for HTML artifacts (check specific types first) // Example: ```html htmlArtifactRegex := regexp.MustCompile(`(?s)` + "```" + `html\s*\s*\n(.*?)\n` + "```") htmlMatches := htmlArtifactRegex.FindAllStringSubmatch(content, -1) for _, match := range htmlMatches { title := strings.TrimSpace(match[1]) artifactContent := strings.TrimSpace(match[2]) artifact := Artifact{ UUID: NewUUID(), Type: "html", Title: title, Content: artifactContent, Language: "html", } artifacts = append(artifacts, artifact) } // Pattern for SVG artifacts // Example: ```svg svgArtifactRegex := regexp.MustCompile(`(?s)` + "```" + `svg\s*\s*\n(.*?)\n` + "```") svgMatches := svgArtifactRegex.FindAllStringSubmatch(content, -1) for _, match := range svgMatches { title := strings.TrimSpace(match[1]) artifactContent := strings.TrimSpace(match[2]) artifact := Artifact{ UUID: NewUUID(), Type: "svg", Title: title, Content: artifactContent, Language: "svg", } artifacts = append(artifacts, artifact) } // Pattern for Mermaid diagrams // Example: ```mermaid mermaidArtifactRegex := regexp.MustCompile(`(?s)` + "```" + `mermaid\s*\s*\n(.*?)\n` + "```") mermaidMatches := mermaidArtifactRegex.FindAllStringSubmatch(content, -1) for _, match := range mermaidMatches { title := strings.TrimSpace(match[1]) artifactContent := strings.TrimSpace(match[2]) artifact := Artifact{ UUID: NewUUID(), Type: "mermaid", Title: title, Content: artifactContent, Language: "mermaid", } artifacts = append(artifacts, artifact) } // Pattern for JSON artifacts // Example: ```json jsonArtifactRegex := regexp.MustCompile(`(?s)` + "```" + `json\s*\s*\n(.*?)\n` + "```") jsonMatches := jsonArtifactRegex.FindAllStringSubmatch(content, -1) for _, match := range jsonMatches { title := strings.TrimSpace(match[1]) artifactContent := strings.TrimSpace(match[2]) artifact := Artifact{ UUID: NewUUID(), Type: "json", Title: title, Content: artifactContent, Language: "json", } artifacts = append(artifacts, artifact) } // Pattern for executable code artifacts // Example: ```javascript executableArtifactRegex := regexp.MustCompile(`(?s)` + "```" + `(\w+)?\s*\s*\n(.*?)\n` + "```") executableMatches := executableArtifactRegex.FindAllStringSubmatch(content, -1) for _, match := range executableMatches { language := match[1] title := strings.TrimSpace(match[2]) artifactContent := strings.TrimSpace(match[3]) // Skip if already processed as HTML, SVG, Mermaid, or JSON if language == "html" || language == "svg" || language == "mermaid" || language == "json" { continue } if language == "" { language = "javascript" // Default to JavaScript for executable code } // Only create executable artifacts for supported languages if isExecutableLanguage(language) { artifact := Artifact{ UUID: NewUUID(), Type: "executable-code", Title: title, Content: artifactContent, Language: language, } artifacts = append(artifacts, artifact) } } // Pattern for general code artifacts (exclude html and svg which are handled above) // Example: ```javascript codeArtifactRegex := regexp.MustCompile(`(?s)` + "```" + `(\w+)?\s*\s*\n(.*?)\n` + "```") matches := codeArtifactRegex.FindAllStringSubmatch(content, -1) for _, match := range matches { language := match[1] title := strings.TrimSpace(match[2]) artifactContent := strings.TrimSpace(match[3]) // Skip if already processed as HTML, SVG, Mermaid, JSON, or executable if language == "html" || language == "svg" || language == "mermaid" || language == "json" { continue } if language == "" { language = "text" } // Check if this should be an executable artifact for supported languages artifactType := "code" if isExecutableLanguage(language) { // For supported languages, make them executable by default if they contain certain patterns if containsExecutablePatterns(artifactContent) { artifactType = "executable-code" } } artifact := Artifact{ UUID: NewUUID(), Type: artifactType, Title: title, Content: artifactContent, Language: language, } artifacts = append(artifacts, artifact) } return artifacts } // isExecutableLanguage checks if a language is supported for code execution func isExecutableLanguage(language string) bool { executableLanguages := []string{ "javascript", "js", "typescript", "ts", "python", "py", } language = strings.ToLower(strings.TrimSpace(language)) for _, execLang := range executableLanguages { if language == execLang { return true } } return false } // containsExecutablePatterns checks if code contains patterns that suggest it should be executable func containsExecutablePatterns(content string) bool { // Patterns that suggest the code is meant to be executed executablePatterns := []string{ // JavaScript patterns "console.log", "console.error", "console.warn", "function", "const ", "let ", "var ", "=>", "if (", "for (", "while (", "return ", // Python patterns "print(", "import ", "from ", "def ", "if __name__", "class ", "for ", "while ", } contentLower := strings.ToLower(content) for _, pattern := range executablePatterns { if strings.Contains(contentLower, pattern) { return true } } return false } ================================================ FILE: api/chat_auth_user_handler.go ================================================ package main import ( "context" "encoding/json" "net/http" "os" "strconv" "strings" "time" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" "github.com/swuecho/chat_backend/auth" "github.com/swuecho/chat_backend/sqlc_queries" ) // Constants for token management const ( AccessTokenLifetime = 30 * time.Minute RefreshTokenLifetime = 7 * 24 * time.Hour // 7 days RefreshTokenName = "refresh_token" ) type AuthUserHandler struct { service *AuthUserService } // isHTTPS checks if the request is using HTTPS or if we're in production func isHTTPS(r *http.Request) bool { // Check if request is HTTPS if r.TLS != nil { return true } // Check common proxy headers if r.Header.Get("X-Forwarded-Proto") == "https" { return true } if r.Header.Get("X-Forwarded-Ssl") == "on" { return true } // Check if environment indicates production env := os.Getenv("ENV") if env == "" { env = os.Getenv("ENVIRONMENT") } if env == "" { env = os.Getenv("NODE_ENV") } return env == "production" || env == "prod" } // createSecureRefreshCookie creates a secure httpOnly cookie for refresh tokens func createSecureRefreshCookie(name, value string, maxAge int, r *http.Request) *http.Cookie { // Determine the appropriate SameSite setting based on environment sameSite := http.SameSiteLaxMode // More permissive for development if isHTTPS(r) { sameSite = http.SameSiteStrictMode // Strict for HTTPS } // Determine domain based on environment var domain string host := r.Host if host != "" && !strings.HasPrefix(host, "localhost") && !strings.HasPrefix(host, "127.0.0.1") { // For production, set domain without port if strings.Contains(host, ":") { domain = strings.Split(host, ":")[0] } else { domain = host } } cookie := &http.Cookie{ Name: name, Value: value, HttpOnly: true, Secure: isHTTPS(r), SameSite: sameSite, Path: "/", MaxAge: maxAge, } // Only set domain if it's not localhost if domain != "" && domain != "localhost" && domain != "127.0.0.1" { cookie.Domain = domain } return cookie } func NewAuthUserHandler(sqlc_q *sqlc_queries.Queries) *AuthUserHandler { userService := NewAuthUserService(sqlc_q) return &AuthUserHandler{service: userService} } func (h *AuthUserHandler) Register(router *mux.Router) { // Authenticated user routes router.HandleFunc("/users", h.GetUserByID).Methods(http.MethodGet) router.HandleFunc("/users/{id}", h.UpdateSelf).Methods(http.MethodPut) router.HandleFunc("/token_10years", h.ForeverToken).Methods(http.MethodGet) } func (h *AuthUserHandler) RegisterPublicRoutes(router *mux.Router) { // Public routes (no authentication required) router.HandleFunc("/signup", h.SignUp).Methods(http.MethodPost) router.HandleFunc("/login", h.Login).Methods(http.MethodPost) router.HandleFunc("/auth/refresh", h.RefreshToken).Methods(http.MethodPost) router.HandleFunc("/logout", h.Logout).Methods(http.MethodPost) } func (h *AuthUserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { var userParams sqlc_queries.CreateAuthUserParams err := json.NewDecoder(r.Body).Decode(&userParams) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } user, err := h.service.CreateAuthUser(r.Context(), userParams) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to create user")) return } json.NewEncoder(w).Encode(user) } func (h *AuthUserHandler) GetUserByID(w http.ResponseWriter, r *http.Request) { userID, err := getUserID(r.Context()) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithDebugInfo(err.Error())) return } user, err := h.service.GetAuthUserByID(r.Context(), userID) if err != nil { RespondWithAPIError(w, ErrResourceNotFound("user")) return } json.NewEncoder(w).Encode(user) } func (h *AuthUserHandler) UpdateSelf(w http.ResponseWriter, r *http.Request) { userID, err := getUserID(r.Context()) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithDebugInfo(err.Error())) return } var userParams sqlc_queries.UpdateAuthUserParams err = json.NewDecoder(r.Body).Decode(&userParams) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } userParams.ID = userID user, err := h.service.q.UpdateAuthUser(r.Context(), userParams) if err != nil { log.WithError(err).Error("Failed to update user") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to update user").WithDebugInfo(err.Error())) return } json.NewEncoder(w).Encode(user) } func (h *AuthUserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) { // get user id from var // to int32 var userParams sqlc_queries.UpdateAuthUserByEmailParams err := json.NewDecoder(r.Body).Decode(&userParams) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } user, err := h.service.q.UpdateAuthUserByEmail(r.Context(), userParams) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update user")) return } json.NewEncoder(w).Encode(user) } type LoginParams struct { Email string `json:"email"` Password string `json:"password"` } func (h *AuthUserHandler) SignUp(w http.ResponseWriter, r *http.Request) { var params LoginParams if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { log.WithFields(log.Fields{ "error": err.Error(), "ip": r.RemoteAddr, "action": "signup_decode_error", }).Warn("Failed to decode signup request") RespondWithAPIError(w, ErrValidationInvalidInput("Invalid request: unable to decode JSON body").WithDebugInfo(err.Error())) return } log.WithFields(log.Fields{ "email": params.Email, "ip": r.RemoteAddr, "action": "signup_attempt", }).Info("User signup attempt") hash, err := auth.GeneratePasswordHash(params.Password) if err != nil { log.WithFields(log.Fields{ "email": params.Email, "error": err.Error(), }).Error("Failed to generate password hash") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to generate password hash").WithDebugInfo(err.Error())) return } userParams := sqlc_queries.CreateAuthUserParams{ Password: hash, Email: params.Email, Username: params.Email, } user, err := h.service.CreateAuthUser(r.Context(), userParams) if err != nil { log.WithFields(log.Fields{ "email": params.Email, "error": err.Error(), }).Error("Failed to create user") RespondWithAPIError(w, WrapError(err, "Failed to create user")) return } // Generate access token using constant tokenString, err := auth.GenerateToken(user.ID, user.Role(), jwtSecretAndAud.Secret, jwtSecretAndAud.Audience, AccessTokenLifetime, auth.TokenTypeAccess) if err != nil { log.WithFields(log.Fields{ "user_id": user.ID, "error": err.Error(), }).Error("Failed to generate access token") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to generate token").WithDebugInfo(err.Error())) return } // Generate refresh token using constant refreshToken, err := auth.GenerateToken(user.ID, user.Role(), jwtSecretAndAud.Secret, jwtSecretAndAud.Audience, RefreshTokenLifetime, auth.TokenTypeRefresh) if err != nil { log.WithFields(log.Fields{ "user_id": user.ID, "error": err.Error(), }).Error("Failed to generate refresh token") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to generate refresh token").WithDebugInfo(err.Error())) return } // Use helper function to create refresh token cookie refreshCookie := createSecureRefreshCookie(RefreshTokenName, refreshToken, int(RefreshTokenLifetime.Seconds()), r) http.SetCookie(w, refreshCookie) log.WithFields(log.Fields{ "user_id": user.ID, "email": user.Email, "action": "signup_success", }).Info("User signup successful") w.Header().Set("Content-Type", "application/json") expiresIn := time.Now().Add(AccessTokenLifetime).Unix() json.NewEncoder(w).Encode(TokenResult{AccessToken: tokenString, ExpiresIn: int(expiresIn)}) w.WriteHeader(http.StatusOK) } func (h *AuthUserHandler) Login(w http.ResponseWriter, r *http.Request) { var loginParams LoginParams err := json.NewDecoder(r.Body).Decode(&loginParams) if err != nil { log.WithFields(log.Fields{ "error": err.Error(), "ip": r.RemoteAddr, "action": "login_decode_error", }).Warn("Failed to decode login request") RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } log.WithFields(log.Fields{ "email": loginParams.Email, "ip": r.RemoteAddr, "action": "login_attempt", }).Info("User login attempt") user, err := h.service.Authenticate(r.Context(), loginParams.Email, loginParams.Password) if err != nil { log.WithFields(log.Fields{ "email": loginParams.Email, "ip": r.RemoteAddr, "error": err.Error(), "action": "login_failed", }).Warn("User login failed") RespondWithAPIError(w, ErrAuthInvalidEmailOrPassword.WithDebugInfo(err.Error())) return } // Generate access token using constant accessToken, err := auth.GenerateToken(user.ID, user.Role(), jwtSecretAndAud.Secret, jwtSecretAndAud.Audience, AccessTokenLifetime, auth.TokenTypeAccess) if err != nil { log.WithFields(log.Fields{ "user_id": user.ID, "error": err.Error(), }).Error("Failed to generate access token") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to generate access token").WithDebugInfo(err.Error())) return } // Generate refresh token using constant refreshToken, err := auth.GenerateToken(user.ID, user.Role(), jwtSecretAndAud.Secret, jwtSecretAndAud.Audience, RefreshTokenLifetime, auth.TokenTypeRefresh) if err != nil { log.WithFields(log.Fields{ "user_id": user.ID, "error": err.Error(), }).Error("Failed to generate refresh token") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to generate refresh token").WithDebugInfo(err.Error())) return } // Use helper function to create refresh token cookie refreshCookie := createSecureRefreshCookie(RefreshTokenName, refreshToken, int(RefreshTokenLifetime.Seconds()), r) http.SetCookie(w, refreshCookie) // Debug: Log cookie details log.WithFields(log.Fields{ "user_id": user.ID, "name": refreshCookie.Name, "domain": refreshCookie.Domain, "path": refreshCookie.Path, "secure": refreshCookie.Secure, "sameSite": refreshCookie.SameSite, "action": "login_cookie_set", }).Info("Refresh token cookie set") log.WithFields(log.Fields{ "user_id": user.ID, "email": user.Email, "action": "login_success", }).Info("User login successful") w.Header().Set("Content-Type", "application/json") expiresIn := time.Now().Add(AccessTokenLifetime).Unix() json.NewEncoder(w).Encode(TokenResult{AccessToken: accessToken, ExpiresIn: int(expiresIn)}) w.WriteHeader(http.StatusOK) } func (h *AuthUserHandler) ForeverToken(w http.ResponseWriter, r *http.Request) { lifetime := time.Duration(10*365*24) * time.Hour userId, _ := getUserID(r.Context()) userRole := r.Context().Value(userContextKey).(string) token, err := auth.GenerateToken(userId, userRole, jwtSecretAndAud.Secret, jwtSecretAndAud.Audience, lifetime, auth.TokenTypeAccess) if err != nil { RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to generate token").WithDebugInfo(err.Error())) return } w.Header().Set("Content-Type", "application/json") expiresIn := time.Now().Add(lifetime).Unix() json.NewEncoder(w).Encode(TokenResult{AccessToken: token, ExpiresIn: int(expiresIn)}) w.WriteHeader(http.StatusOK) } func (h *AuthUserHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { log.WithFields(log.Fields{ "ip": r.RemoteAddr, "action": "refresh_attempt", }).Info("Token refresh attempt") // Debug: Log all cookies to help diagnose the issue allCookies := r.Cookies() log.WithFields(log.Fields{ "ip": r.RemoteAddr, "cookies": len(allCookies), "action": "refresh_debug_cookies", }).Info("All cookies received") for _, cookie := range allCookies { log.WithFields(log.Fields{ "ip": r.RemoteAddr, "name": cookie.Name, "domain": cookie.Domain, "path": cookie.Path, "action": "refresh_debug_cookie", }).Info("Cookie details") } // Get refresh token from httpOnly cookie refreshCookie, err := r.Cookie(RefreshTokenName) if err != nil { log.WithFields(log.Fields{ "ip": r.RemoteAddr, "error": err.Error(), "action": "refresh_missing_cookie", }).Warn("Missing refresh token cookie") RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("Missing refresh token")) return } // Validate refresh token result := parseAndValidateJWT(refreshCookie.Value, auth.TokenTypeRefresh) if result.Error != nil { log.WithFields(log.Fields{ "ip": r.RemoteAddr, "error": result.Error.Detail, "action": "refresh_invalid_token", }).Warn("Invalid refresh token") RespondWithAPIError(w, *result.Error) return } // Convert UserID string back to int32 userIDInt, err := strconv.ParseInt(result.UserID, 10, 32) if err != nil { log.WithFields(log.Fields{ "user_id": result.UserID, "error": err.Error(), "action": "refresh_invalid_user_id", }).Error("Invalid user ID in refresh token") RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("Invalid user ID in token")) return } // Generate new access token using constant accessToken, err := auth.GenerateToken(int32(userIDInt), result.Role, jwtSecretAndAud.Secret, jwtSecretAndAud.Audience, AccessTokenLifetime, auth.TokenTypeAccess) if err != nil { log.WithFields(log.Fields{ "user_id": userIDInt, "error": err.Error(), }).Error("Failed to generate access token during refresh") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to generate access token").WithDebugInfo(err.Error())) return } log.WithFields(log.Fields{ "user_id": userIDInt, "action": "refresh_success", }).Info("Token refresh successful") w.Header().Set("Content-Type", "application/json") expiresIn := time.Now().Add(AccessTokenLifetime).Unix() json.NewEncoder(w).Encode(TokenResult{AccessToken: accessToken, ExpiresIn: int(expiresIn)}) } func (h *AuthUserHandler) Logout(w http.ResponseWriter, r *http.Request) { log.WithFields(log.Fields{ "ip": r.RemoteAddr, "action": "logout_attempt", }).Info("User logout attempt") // Clear refresh token cookie using the same domain logic as creation refreshCookie := createSecureRefreshCookie(RefreshTokenName, "", -1, r) http.SetCookie(w, refreshCookie) log.WithFields(log.Fields{ "ip": r.RemoteAddr, "action": "logout_success", }).Info("User logout successful") w.WriteHeader(http.StatusOK) } type TokenRequest struct { Token string `json:"token"` } type ResetPasswordRequest struct { Email string `json:"email"` } func (h *AuthUserHandler) ResetPasswordHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } var req ResetPasswordRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } // Retrieve user account from the database by email address user, err := h.service.q.GetUserByEmail(context.Background(), req.Email) if err != nil { RespondWithAPIError(w, ErrResourceNotFound("user")) return } // Generate temporary password tempPassword, err := auth.GenerateRandomPassword() if err != nil { log.WithError(err).Error("Failed to generate temporary password") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to generate temporary password").WithDebugInfo(err.Error())) return } // Hash temporary password hashedPassword, err := auth.GeneratePasswordHash(tempPassword) if err != nil { RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to hash password").WithDebugInfo(err.Error())) return } // Update user account with new hashed password err = h.service.q.UpdateUserPassword( context.Background(), sqlc_queries.UpdateUserPasswordParams{ Email: req.Email, Password: hashedPassword, }) if err != nil { RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to update password").WithDebugInfo(err.Error())) return } // Send email to the user with temporary password and instructions err = SendPasswordResetEmail(user.Email, tempPassword) if err != nil { RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to send password reset email").WithDebugInfo(err.Error())) return } w.WriteHeader(http.StatusOK) } func SendPasswordResetEmail(email, tempPassword string) error { return nil } type ChangePasswordRequest struct { Email string `json:"email"` NewPassword string `json:"new_password"` } func (h *AuthUserHandler) ChangePasswordHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } var req ChangePasswordRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } // Hash new password hashedPassword, err := auth.GeneratePasswordHash(req.NewPassword) if err != nil { RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to hash password").WithDebugInfo(err.Error())) return } // Update password in the database err = h.service.q.UpdateUserPassword(context.Background(), sqlc_queries.UpdateUserPasswordParams{ Email: req.Email, Password: string(hashedPassword), }) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update password")) return } w.WriteHeader(http.StatusOK) } type UserStat struct { Email string `json:"email"` FirstName string `json:"firstName"` LastName string `json:"lastName"` TotalChatMessages int64 `json:"totalChatMessages"` TotalChatMessagesTokenCount int64 `json:"totalChatMessagesTokenCount"` TotalChatMessages3Days int64 `json:"totalChatMessages3Days"` TotalChatMessages3DaysTokenCount int64 `json:"totalChatMessages3DaysTokenCount"` AvgChatMessages3DaysTokenCount int64 `json:"avgChatMessages3DaysTokenCount"` RateLimit int32 `json:"rateLimit"` } func (h *AuthUserHandler) UserStatHandler(w http.ResponseWriter, r *http.Request) { var pagination Pagination err := json.NewDecoder(r.Body).Decode(&pagination) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } userStatsRows, total, err := h.service.GetUserStats(r.Context(), pagination, int32(appConfig.OPENAI.RATELIMIT)) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get user stats")) return } // Create a new []interface{} slice with same length as userStatsRows data := make([]interface{}, len(userStatsRows)) // Copy the contents of userStatsRows into data for i, v := range userStatsRows { divider := v.TotalChatMessages3Days var avg int64 if divider > 0 { avg = v.TotalTokenCount3Days / v.TotalChatMessages3Days } else { avg = 0 } data[i] = UserStat{ Email: v.UserEmail, FirstName: v.FirstName, LastName: v.LastName, TotalChatMessages: v.TotalChatMessages, TotalChatMessages3Days: v.TotalChatMessages3Days, RateLimit: v.RateLimit, TotalChatMessagesTokenCount: v.TotalTokenCount, TotalChatMessages3DaysTokenCount: v.TotalTokenCount3Days, AvgChatMessages3DaysTokenCount: avg, } } json.NewEncoder(w).Encode(Pagination{ Page: pagination.Page, Size: pagination.Size, Total: total, Data: data, }) } type RateLimitRequest struct { Email string `json:"email"` RateLimit int32 `json:"rateLimit"` } func (h *AuthUserHandler) UpdateRateLimit(w http.ResponseWriter, r *http.Request) { var rateLimitRequest RateLimitRequest err := json.NewDecoder(r.Body).Decode(&rateLimitRequest) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } rate, err := h.service.q.UpdateAuthUserRateLimitByEmail(r.Context(), sqlc_queries.UpdateAuthUserRateLimitByEmailParams{ Email: rateLimitRequest.Email, RateLimit: rateLimitRequest.RateLimit, }) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update rate limit")) return } json.NewEncoder(w).Encode( map[string]int32{ "rate": rate, }) } func (h *AuthUserHandler) GetRateLimit(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithDebugInfo(err.Error())) return } rate, err := h.service.q.GetRateLimit(ctx, userID) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get rate limit")) return } json.NewEncoder(w).Encode(map[string]int32{ "rate": rate, }) } ================================================ FILE: api/chat_auth_user_service.go ================================================ package main import ( "context" "errors" "fmt" "net/http" "strconv" "time" "github.com/rotisserie/eris" "github.com/swuecho/chat_backend/auth" "github.com/swuecho/chat_backend/sqlc_queries" ) type AuthUserService struct { q *sqlc_queries.Queries } // NewAuthUserService creates a new AuthUserService. func NewAuthUserService(q *sqlc_queries.Queries) *AuthUserService { return &AuthUserService{q: q} } // CreateAuthUser creates a new authentication user record. func (s *AuthUserService) CreateAuthUser(ctx context.Context, auth_user_params sqlc_queries.CreateAuthUserParams) (sqlc_queries.AuthUser, error) { totalUserCount, err := s.q.GetTotalActiveUserCount(ctx) if err != nil { return sqlc_queries.AuthUser{}, errors.New("failed to retrieve total user count") } if totalUserCount == 0 { auth_user_params.IsSuperuser = true fmt.Println("First user is superuser.") } auth_user, err := s.q.CreateAuthUser(ctx, auth_user_params) if err != nil { return sqlc_queries.AuthUser{}, err } return auth_user, nil } // GetAuthUserByID returns an authentication user record by ID. func (s *AuthUserService) GetAuthUserByID(ctx context.Context, id int32) (sqlc_queries.AuthUser, error) { auth_user, err := s.q.GetAuthUserByID(ctx, id) if err != nil { return sqlc_queries.AuthUser{}, errors.New("failed to retrieve authentication user") } return auth_user, nil } // GetAllAuthUsers returns all authentication user records. func (s *AuthUserService) GetAllAuthUsers(ctx context.Context) ([]sqlc_queries.AuthUser, error) { auth_users, err := s.q.GetAllAuthUsers(ctx) if err != nil { return nil, errors.New("failed to retrieve authentication users") } return auth_users, nil } func (s *AuthUserService) Authenticate(ctx context.Context, email, password string) (sqlc_queries.AuthUser, error) { user, err := s.q.GetUserByEmail(ctx, email) if err != nil { return sqlc_queries.AuthUser{}, err } if !auth.ValidatePassword(password, user.Password) { return sqlc_queries.AuthUser{}, ErrAuthInvalidCredentials } return user, nil } func (s *AuthUserService) Logout(tokenString string) (*http.Cookie, error) { userID, err := auth.ValidateToken(tokenString, jwtSecretAndAud.Secret, auth.TokenTypeAccess) if err != nil { return nil, err } // Implement a mechanism to track invalidated tokens for the given user ID // auth.AddInvalidToken(userID, "insert-invalidated-token-here") cookie := auth.GetExpireSecureCookie(strconv.Itoa(int(userID)), false) return cookie, nil } // backend api // GetUserStat(page, page_size) -> {data: [{user_email, total_sessions, total_messages, total_sessions_3_days, total_messages_3_days, rate_limit}], total: 100} // GetTotalUserCount // GetUserStat(page, page_size) ->[{user_email, total_sessions, total_messages, total_sessions_3_days, total_messages_3_days, rate_limit}] func (s *AuthUserService) GetUserStats(ctx context.Context, p Pagination, defaultRateLimit int32) ([]sqlc_queries.GetUserStatsRow, int64, error) { auth_users_stat, err := s.q.GetUserStats(ctx, sqlc_queries.GetUserStatsParams{ Offset: p.Offset(), Limit: p.Size, DefaultRateLimit: defaultRateLimit, }) if err != nil { return nil, 0, eris.Wrap(err, "failed to retrieve user stats ") } total, err := s.q.GetTotalActiveUserCount(ctx) if err != nil { return nil, 0, errors.New("failed to retrieve total active user count") } return auth_users_stat, total, nil } // UpdateRateLimit(user_email, rate_limit) -> { rate_limit: 100 } func (s *AuthUserService) UpdateRateLimit(ctx context.Context, user_email string, rate_limit int32) (int32, error) { auth_user_params := sqlc_queries.UpdateAuthUserRateLimitByEmailParams{ Email: user_email, RateLimit: rate_limit, } rate, err := s.q.UpdateAuthUserRateLimitByEmail(ctx, auth_user_params) if err != nil { return -1, errors.New("failed to update authentication user") } return rate, nil } // get ratelimit for user_id func (s *AuthUserService) GetRateLimit(ctx context.Context, user_id int32) (int32, error) { rate, err := s.q.GetRateLimit(ctx, user_id) if err != nil { return -1, errors.New("failed to get rate limit") } return rate, nil } // UserAnalysisData represents the complete user analysis response type UserAnalysisData struct { UserInfo UserAnalysisInfo `json:"userInfo"` ModelUsage []ModelUsageInfo `json:"modelUsage"` RecentActivity []ActivityInfo `json:"recentActivity"` } type UserAnalysisInfo struct { Email string `json:"email"` TotalMessages int64 `json:"totalMessages"` TotalTokens int64 `json:"totalTokens"` TotalSessions int64 `json:"totalSessions"` Messages3Days int64 `json:"messages3Days"` Tokens3Days int64 `json:"tokens3Days"` RateLimit int32 `json:"rateLimit"` } type ModelUsageInfo struct { Model string `json:"model"` MessageCount int64 `json:"messageCount"` TokenCount int64 `json:"tokenCount"` Percentage float64 `json:"percentage"` LastUsed time.Time `json:"lastUsed"` } type ActivityInfo struct { Date time.Time `json:"date"` Messages int64 `json:"messages"` Tokens int64 `json:"tokens"` Sessions int64 `json:"sessions"` } type SessionHistoryInfo struct { SessionID string `json:"sessionId"` Model string `json:"model"` MessageCount int64 `json:"messageCount"` TokenCount int64 `json:"tokenCount"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // GetUserAnalysis retrieves comprehensive user analysis data func (s *AuthUserService) GetUserAnalysis(ctx context.Context, email string, defaultRateLimit int32) (*UserAnalysisData, error) { // Get basic user info userInfo, err := s.q.GetUserAnalysisByEmail(ctx, sqlc_queries.GetUserAnalysisByEmailParams{ Email: email, DefaultRateLimit: defaultRateLimit, }) if err != nil { return nil, eris.Wrap(err, "failed to get user analysis") } // Get model usage modelUsageRows, err := s.q.GetUserModelUsageByEmail(ctx, email) if err != nil { return nil, eris.Wrap(err, "failed to get user model usage") } // Calculate total tokens for percentage calculation var totalTokens int64 for _, row := range modelUsageRows { if row.TokenCount != nil { if tc, ok := row.TokenCount.(int64); ok { totalTokens += tc } } } modelUsage := make([]ModelUsageInfo, len(modelUsageRows)) for i, row := range modelUsageRows { // Convert interface{} to int64 safely tokenCount := int64(0) if row.TokenCount != nil { if tc, ok := row.TokenCount.(int64); ok { tokenCount = tc } } percentage := float64(0) if totalTokens > 0 { percentage = float64(tokenCount) / float64(totalTokens) * 100 } modelUsage[i] = ModelUsageInfo{ Model: row.Model, MessageCount: row.MessageCount, TokenCount: tokenCount, Percentage: percentage, LastUsed: row.LastUsed, } } // Get recent activity activityRows, err := s.q.GetUserRecentActivityByEmail(ctx, email) if err != nil { return nil, eris.Wrap(err, "failed to get user recent activity") } recentActivity := make([]ActivityInfo, len(activityRows)) for i, row := range activityRows { // Convert interface{} to int64 safely tokens := int64(0) if row.Tokens != nil { if t, ok := row.Tokens.(int64); ok { tokens = t } } recentActivity[i] = ActivityInfo{ Date: row.ActivityDate, Messages: row.Messages, Tokens: tokens, Sessions: row.Sessions, } } analysisData := &UserAnalysisData{ UserInfo: UserAnalysisInfo{ Email: userInfo.UserEmail, TotalMessages: userInfo.TotalMessages, TotalTokens: userInfo.TotalTokens, TotalSessions: userInfo.TotalSessions, Messages3Days: userInfo.Messages3Days, Tokens3Days: userInfo.Tokens3Days, RateLimit: userInfo.RateLimit, }, ModelUsage: modelUsage, RecentActivity: recentActivity, } return analysisData, nil } // GetUserSessionHistory retrieves paginated session history for a user func (s *AuthUserService) GetUserSessionHistory(ctx context.Context, email string, page, pageSize int32) ([]SessionHistoryInfo, int64, error) { offset := (page - 1) * pageSize // Get session history with pagination sessionRows, err := s.q.GetUserSessionHistoryByEmail(ctx, sqlc_queries.GetUserSessionHistoryByEmailParams{ Email: email, Limit: pageSize, Offset: offset, }) if err != nil { return nil, 0, eris.Wrap(err, "failed to get user session history") } // Get total count totalCount, err := s.q.GetUserSessionHistoryCountByEmail(ctx, email) if err != nil { return nil, 0, eris.Wrap(err, "failed to get user session history count") } sessionHistory := make([]SessionHistoryInfo, len(sessionRows)) for i, row := range sessionRows { // Convert interface{} to int64 safely messageCount := int64(0) if row.MessageCount != nil { if mc, ok := row.MessageCount.(int64); ok { messageCount = mc } } tokenCount := int64(0) if row.TokenCount != nil { if tc, ok := row.TokenCount.(int64); ok { tokenCount = tc } } sessionHistory[i] = SessionHistoryInfo{ SessionID: row.SessionID, Model: row.Model, MessageCount: messageCount, TokenCount: tokenCount, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } return sessionHistory, totalCount, nil } ================================================ FILE: api/chat_comment_handler.go ================================================ package main import ( "encoding/json" "net/http" "github.com/google/uuid" "github.com/gorilla/mux" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatCommentHandler struct { service *ChatCommentService } func NewChatCommentHandler(sqlc_q *sqlc_queries.Queries) *ChatCommentHandler { chatCommentService := NewChatCommentService(sqlc_q) return &ChatCommentHandler{ service: chatCommentService, } } func (h *ChatCommentHandler) Register(router *mux.Router) { router.HandleFunc("/uuid/chat_sessions/{sessionUUID}/chat_messages/{messageUUID}/comments", h.CreateChatComment).Methods(http.MethodPost) router.HandleFunc("/uuid/chat_sessions/{sessionUUID}/comments", h.GetCommentsBySessionUUID).Methods(http.MethodGet) router.HandleFunc("/uuid/chat_messages/{messageUUID}/comments", h.GetCommentsByMessageUUID).Methods(http.MethodGet) } func (h *ChatCommentHandler) CreateChatComment(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) sessionUUID := vars["sessionUUID"] messageUUID := vars["messageUUID"] var req struct { Content string `json:"content"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } userID, err := getUserID(r.Context()) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("unauthorized").WithDebugInfo(err.Error())) return } comment, err := h.service.CreateChatComment(r.Context(), sqlc_queries.CreateChatCommentParams{ Uuid: uuid.New().String(), ChatSessionUuid: sessionUUID, ChatMessageUuid: messageUUID, Content: req.Content, CreatedBy: userID, }) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to create chat comment")) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(comment) } func (h *ChatCommentHandler) GetCommentsBySessionUUID(w http.ResponseWriter, r *http.Request) { sessionUUID := mux.Vars(r)["sessionUUID"] comments, err := h.service.GetCommentsBySessionUUID(r.Context(), sessionUUID) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get comments by session")) return } json.NewEncoder(w).Encode(comments) } func (h *ChatCommentHandler) GetCommentsByMessageUUID(w http.ResponseWriter, r *http.Request) { messageUUID := mux.Vars(r)["messageUUID"] comments, err := h.service.GetCommentsByMessageUUID(r.Context(), messageUUID) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get comments by message")) return } json.NewEncoder(w).Encode(comments) } ================================================ FILE: api/chat_comment_service.go ================================================ package main import ( "context" "time" "github.com/rotisserie/eris" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatCommentService struct { q *sqlc_queries.Queries } func NewChatCommentService(q *sqlc_queries.Queries) *ChatCommentService { return &ChatCommentService{q: q} } // CreateChatComment creates a new chat comment func (s *ChatCommentService) CreateChatComment(ctx context.Context, params sqlc_queries.CreateChatCommentParams) (sqlc_queries.ChatComment, error) { comment, err := s.q.CreateChatComment(ctx, params) if err != nil { return sqlc_queries.ChatComment{}, eris.Wrap(err, "failed to create comment") } return comment, nil } // GetCommentsBySessionUUID returns comments for a session with author info func (s *ChatCommentService) GetCommentsBySessionUUID(ctx context.Context, sessionUUID string) ([]sqlc_queries.GetCommentsBySessionUUIDRow, error) { comments, err := s.q.GetCommentsBySessionUUID(ctx, sessionUUID) if err != nil { return nil, eris.Wrap(err, "failed to get comments by session UUID") } return comments, nil } // GetCommentsByMessageUUID returns comments for a message with author info func (s *ChatCommentService) GetCommentsByMessageUUID(ctx context.Context, messageUUID string) ([]sqlc_queries.GetCommentsByMessageUUIDRow, error) { comments, err := s.q.GetCommentsByMessageUUID(ctx, messageUUID) if err != nil { return nil, eris.Wrap(err, "failed to get comments by message UUID") } return comments, nil } // CommentWithAuthor represents a comment with author information type CommentWithAuthor struct { UUID string `json:"uuid"` Content string `json:"content"` CreatedAt time.Time `json:"createdAt"` AuthorUsername string `json:"authorUsername"` AuthorEmail string `json:"authorEmail"` } // GetCommentsBySession returns comments for a session with author info func (s *ChatCommentService) GetCommentsBySession(ctx context.Context, sessionUUID string) ([]CommentWithAuthor, error) { comments, err := s.q.GetCommentsBySessionUUID(ctx, sessionUUID) if err != nil { return nil, eris.Wrap(err, "failed to get comments by session") } result := make([]CommentWithAuthor, len(comments)) for i, c := range comments { result[i] = CommentWithAuthor{ UUID: c.Uuid, Content: c.Content, CreatedAt: c.CreatedAt, AuthorUsername: c.AuthorUsername, AuthorEmail: c.AuthorEmail, } } return result, nil } // GetCommentsByMessage returns comments for a message with author info func (s *ChatCommentService) GetCommentsByMessage(ctx context.Context, messageUUID string) ([]CommentWithAuthor, error) { comments, err := s.q.GetCommentsByMessageUUID(ctx, messageUUID) if err != nil { return nil, eris.Wrap(err, "failed to get comments by message") } result := make([]CommentWithAuthor, len(comments)) for i, c := range comments { result[i] = CommentWithAuthor{ UUID: c.Uuid, Content: c.Content, CreatedAt: c.CreatedAt, AuthorUsername: c.AuthorUsername, AuthorEmail: c.AuthorEmail, } } return result, nil } ================================================ FILE: api/chat_main_handler.go ================================================ package main import ( "context" "database/sql" "encoding/json" "errors" "fmt" log "github.com/sirupsen/logrus" "net/http" "strings" "time" mapset "github.com/deckarep/golang-set/v2" openai "github.com/sashabaranov/go-openai" "github.com/gorilla/mux" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatHandler struct { service *ChatService chatfileService *ChatFileService requestCtx context.Context // Store the request context for streaming } const sessionTitleGenerationTimeout = 30 * time.Second func NewChatHandler(sqlc_q *sqlc_queries.Queries) *ChatHandler { // create a new ChatService instance chatService := NewChatService(sqlc_q) ChatFileService := NewChatFileService(sqlc_q) return &ChatHandler{ service: chatService, chatfileService: ChatFileService, requestCtx: context.Background(), } } func (h *ChatHandler) Register(router *mux.Router) { router.HandleFunc("/chat_stream", h.ChatCompletionHandler).Methods(http.MethodPost) // for bot // given a chat_uuid, a user message, return the answer // router.HandleFunc("/chatbot", h.ChatBotCompletionHandler).Methods(http.MethodPost) router.HandleFunc("/chat_instructions", h.GetChatInstructions).Methods(http.MethodGet) } type ChatRequest struct { Prompt string SessionUuid string ChatUuid string Regenerate bool Stream bool `json:"stream,omitempty"` } type ChatCompletionResponse struct { ID string `json:"id"` Object string `json:"object"` Created int `json:"created"` Model string `json:"model"` Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } `json:"usage"` Choices []Choice `json:"choices"` } type Choice struct { Message openai.ChatCompletionMessage `json:"message"` FinishReason any `json:"finish_reason"` Index int `json:"index"` } type OpenaiChatRequest struct { Model string `json:"model"` Messages []openai.ChatCompletionMessage `json:"messages"` } type BotRequest struct { Message string `json:"message"` SnapshotUuid string `json:"snapshot_uuid"` Stream bool `json:"stream"` } type ChatInstructionResponse struct { ArtifactInstruction string `json:"artifactInstruction"` } func (h *ChatHandler) GetChatInstructions(w http.ResponseWriter, r *http.Request) { artifactInstruction, err := loadArtifactInstruction() if err != nil { log.Printf("Warning: Failed to load artifact instruction: %v", err) artifactInstruction = "" } json.NewEncoder(w).Encode(ChatInstructionResponse{ ArtifactInstruction: artifactInstruction, }) } // ChatCompletionHandler is an HTTP handler that sends the stream to the client as Server-Sent Events (SSE) func (h *ChatHandler) ChatBotCompletionHandler(w http.ResponseWriter, r *http.Request) { var req BotRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } snapshotUuid := req.SnapshotUuid newQuestion := req.Message log.Printf("snapshotUuid: %s", snapshotUuid) log.Printf("newQuestion: %s", newQuestion) ctx := r.Context() userID, err := getUserID(ctx) if err != nil { log.Printf("Error getting user ID: %v", err) apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } fmt.Printf("userID: %d", userID) chatSnapshot, err := h.service.q.ChatSnapshotByUserIdAndUuid(ctx, sqlc_queries.ChatSnapshotByUserIdAndUuidParams{ UserID: userID, Uuid: snapshotUuid, }) if err != nil { apiErr := ErrResourceNotFound("Chat snapshot") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } fmt.Printf("chatSnapshot: %+v", chatSnapshot) var session sqlc_queries.ChatSession err = json.Unmarshal(chatSnapshot.Session, &session) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to deserialize chat session" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } var simpleChatMessages []SimpleChatMessage err = json.Unmarshal(chatSnapshot.Conversation, &simpleChatMessages) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to deserialize conversation" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } genBotAnswer(h, w, session, simpleChatMessages, snapshotUuid, newQuestion, userID, req.Stream) } // ChatCompletionHandler is an HTTP handler that sends the stream to the client as Server-Sent Events (SSE) func (h *ChatHandler) ChatCompletionHandler(w http.ResponseWriter, r *http.Request) { var req ChatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Printf("Error decoding request: %v", err) apiErr := ErrValidationInvalidInput("Invalid request format") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } chatSessionUuid := req.SessionUuid chatUuid := req.ChatUuid newQuestion := req.Prompt log.Printf("chatSessionUuid: %s", chatSessionUuid) log.Printf("chatUuid: %s", chatUuid) log.Printf("newQuestion: %s", newQuestion) ctx := r.Context() userID, err := getUserID(ctx) if err != nil { log.Printf("Error getting user ID: %v", err) apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } if req.Regenerate { regenerateAnswer(h, w, ctx, chatSessionUuid, chatUuid, req.Stream) } else { genAnswer(h, w, ctx, chatSessionUuid, chatUuid, newQuestion, userID, req.Stream) } } // validateChatSession validates the chat session and returns the session and model info. // It performs comprehensive validation including: // - Session existence check // - Model availability verification // - Base URL extraction // - UUID validation // Returns: session, model, baseURL, success func (h *ChatHandler) validateChatSession(ctx context.Context, w http.ResponseWriter, chatSessionUuid string) (*sqlc_queries.ChatSession, *sqlc_queries.ChatModel, string, bool) { chatSession, err := h.service.q.GetChatSessionByUUID(ctx, chatSessionUuid) if err != nil { log.Printf("Invalid session UUID: %s, error: %v", chatSessionUuid, err) RespondWithAPIError(w, ErrResourceNotFound("chat session").WithMessage(chatSessionUuid)) return nil, nil, "", false } chatModel, err := h.service.q.ChatModelByName(ctx, chatSession.Model) if err != nil { RespondWithAPIError(w, ErrResourceNotFound("chat model: "+chatSession.Model)) return nil, nil, "", false } baseURL, _ := getModelBaseUrl(chatModel.Url) if chatSession.Uuid == "" { log.Printf("Empty session UUID for chat: %s", chatSessionUuid) RespondWithAPIError(w, ErrValidationInvalidInput("Invalid session UUID")) return nil, nil, "", false } return &chatSession, &chatModel, baseURL, true } // handlePromptCreation handles creating new prompt or adding user message to existing conversation. // This function manages the logic for: // - Detecting existing prompts in the session // - Creating new prompts for fresh conversations // - Adding user messages to ongoing conversations // - Handling empty questions for regeneration scenarios func (h *ChatHandler) handlePromptCreation(ctx context.Context, w http.ResponseWriter, chatSession *sqlc_queries.ChatSession, chatUuid, newQuestion string, userID int32, baseURL string) bool { existingPrompt := true prompt, err := h.service.q.GetOneChatPromptBySessionUUID(ctx, chatSession.Uuid) if err != nil { if errors.Is(err, sql.ErrNoRows) { log.Printf("No existing prompt found for session: %s", chatSession.Uuid) existingPrompt = false } else { log.Printf("Error checking prompt for session %s: %v", chatSession.Uuid, err) RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to get prompt", err.Error())) return false } } else { log.Printf("Found existing prompt ID %d for session %s", prompt.ID, chatSession.Uuid) } if existingPrompt { if newQuestion != "" { _, err := h.service.CreateChatMessageSimple(ctx, chatSession.Uuid, chatUuid, "user", newQuestion, "", chatSession.Model, userID, baseURL, chatSession.SummarizeMode) if err != nil { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to create message", err.Error())) return false } } else { log.Println("no new question, regenerate answer") } } else { chatPrompt, err := h.service.CreateChatPromptSimple(ctx, chatSession.Uuid, DefaultSystemPromptText, userID) if err != nil { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to create prompt", err.Error())) return false } log.Printf("%+v\n", chatPrompt) if newQuestion != "" { _, err := h.service.CreateChatMessageSimple(ctx, chatSession.Uuid, chatUuid, "user", newQuestion, "", chatSession.Model, userID, baseURL, chatSession.SummarizeMode) if err != nil { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to create message", err.Error())) return false } } // Update session title with first 10 words of the first user message. if newQuestion != "" { sessionTitle := firstNWords(newQuestion, 10) if sessionTitle != "" { updateParams := sqlc_queries.UpdateChatSessionTopicByUUIDParams{ Uuid: chatSession.Uuid, UserID: userID, Topic: sessionTitle, } _, err := h.service.q.UpdateChatSessionTopicByUUID(ctx, updateParams) if err != nil { log.Printf("Warning: Failed to update session title for session %s: %v", chatSession.Uuid, err) } else { log.Printf("Updated session %s title to: %s", chatSession.Uuid, sessionTitle) } } } } return true } // generateAndSaveAnswer generates the LLM response and saves it to the database func (h *ChatHandler) generateAndSaveAnswer(ctx context.Context, w http.ResponseWriter, chatSession *sqlc_queries.ChatSession, chatUuid string, userID int32, baseURL string, streamOutput bool) bool { msgs, err := h.service.getAskMessages(*chatSession, chatUuid, false) if err != nil { log.Printf("Error collecting messages for session %s: %v", chatSession.Uuid, err) RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to collect messages", err.Error())) return false } log.Printf("Collected messages for processing - SessionUUID: %s, MessageCount: %d, Model: %s", chatSession.Uuid, len(msgs), chatSession.Model) // Store the request context so models can access it h.requestCtx = ctx model := h.chooseChatModel(*chatSession, msgs) LLMAnswer, err := model.Stream(w, *chatSession, msgs, chatUuid, false, streamOutput) if err != nil { log.Printf("Error generating answer: %v", err) RespondWithAPIError(w, WrapError(err, "Failed to generate answer")) return false } if LLMAnswer == nil { log.Printf("Error generating answer: LLMAnswer is nil") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("LLMAnswer is nil")) return false } if !isTest(msgs) { log.Printf("LLMAnswer: %+v", LLMAnswer) h.service.logChat(*chatSession, msgs, LLMAnswer.ReasoningContent+LLMAnswer.Answer) } chatMessage, err := h.service.CreateChatMessageWithSuggestedQuestions(ctx, chatSession.Uuid, LLMAnswer.AnswerId, "assistant", LLMAnswer.Answer, LLMAnswer.ReasoningContent, chatSession.Model, userID, baseURL, chatSession.SummarizeMode, chatSession.ExploreMode, msgs) if err != nil { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to create message", err.Error())) return false } // Send suggested questions as a separate streaming event if streaming is enabled and exploreMode is on if streamOutput && chatSession.ExploreMode && chatMessage.SuggestedQuestions != nil { h.sendSuggestedQuestionsStream(w, LLMAnswer.AnswerId, chatMessage.SuggestedQuestions) } // Generate a better title using LLM for the first exchange (async, non-blocking). // Detach from the request context so the follow-up DB/model call can finish // after the streaming response closes. go h.generateSessionTitle(chatSession, userID) return true } // generateSessionTitle regenerates the session title from the latest conversation state. func (h *ChatHandler) generateSessionTitle(chatSession *sqlc_queries.ChatSession, userID int32) { ctx, cancel := context.WithTimeout(context.Background(), sessionTitleGenerationTimeout) defer cancel() // Regenerate from the latest conversation after each assistant response. messages, err := h.service.q.GetChatMessagesBySessionUUID(ctx, sqlc_queries.GetChatMessagesBySessionUUIDParams{ Uuid: chatSession.Uuid, Offset: 0, Limit: 100, }) if err != nil { log.Printf("Warning: Failed to get messages for title generation: %v", err) return } var chatText string for _, msg := range messages { if msg.Role == "user" { chatText += "user: " + msg.Content + "\n" } else if msg.Role == "assistant" { chatText += "assistant: " + msg.Content + "\n" } } if strings.TrimSpace(chatText) == "" { return } // Use the same approach as chat_snapshot_service.go - check if gemini-2.0-flash is available model := "gemini-2.0-flash" _, err = h.service.q.ChatModelByName(ctx, model) if err != nil { // Model not available, skip title generation return } // Generate title using Gemini genTitle, err := GenerateChatTitle(ctx, model, chatText) if err != nil { log.Printf("Warning: Failed to generate session title: %v", err) return } if genTitle == "" { return } // Update the session title updateParams := sqlc_queries.UpdateChatSessionTopicByUUIDParams{ Uuid: chatSession.Uuid, UserID: userID, Topic: genTitle, } _, err = h.service.q.UpdateChatSessionTopicByUUID(ctx, updateParams) if err != nil { log.Printf("Warning: Failed to update session title: %v", err) return } log.Printf("Generated LLM title for session %s: %s", chatSession.Uuid, genTitle) } // sendSuggestedQuestionsStream sends suggested questions as a separate streaming event func (h *ChatHandler) sendSuggestedQuestionsStream(w http.ResponseWriter, answerID string, suggestedQuestionsJSON json.RawMessage) { // Parse the suggested questions JSON var suggestedQuestions []string if err := json.Unmarshal(suggestedQuestionsJSON, &suggestedQuestions); err != nil { log.Printf("Warning: Failed to parse suggested questions for streaming: %v", err) return } // Only send if we have questions if len(suggestedQuestions) == 0 { return } // Get the flusher for streaming flusher, ok := w.(http.Flusher) if !ok { log.Printf("Warning: Response writer does not support flushing, cannot send suggested questions stream") return } // Create a special response with suggested questions suggestedQuestionsResponse := map[string]interface{}{ "id": answerID, "object": "chat.completion.chunk", "choices": []map[string]interface{}{ { "index": 0, "delta": map[string]interface{}{ "content": "", // Empty content "suggestedQuestions": suggestedQuestions, }, "finish_reason": nil, }, }, } data, err := json.Marshal(suggestedQuestionsResponse) if err != nil { log.Printf("Warning: Failed to marshal suggested questions response: %v", err) return } // Send the streaming event fmt.Fprintf(w, "data: %v\n\n", string(data)) flusher.Flush() log.Printf("Sent suggested questions stream for answer ID: %s, questions: %v", answerID, suggestedQuestions) } // genAnswer is an HTTP handler that sends the stream to the client as Server-Sent Events (SSE) // if there is no prompt yet, it will create a new prompt and use it as request // otherwise, it will create a message, use prompt + get latest N message + newQuestion as request func genAnswer(h *ChatHandler, w http.ResponseWriter, ctx context.Context, chatSessionUuid string, chatUuid string, newQuestion string, userID int32, streamOutput bool) { // Validate chat session and get model info chatSession, _, baseURL, ok := h.validateChatSession(ctx, w, chatSessionUuid) if !ok { return } log.Printf("Processing chat session - SessionUUID: %s, UserID: %d, Model: %s", chatSession.Uuid, userID, chatSession.Model) // Handle prompt creation or user message addition if !h.handlePromptCreation(ctx, w, chatSession, chatUuid, newQuestion, userID, baseURL) { return } // Generate and save the answer h.generateAndSaveAnswer(ctx, w, chatSession, chatUuid, userID, baseURL, streamOutput) } func genBotAnswer(h *ChatHandler, w http.ResponseWriter, session sqlc_queries.ChatSession, simpleChatMessages []SimpleChatMessage, snapshotUuid, newQuestion string, userID int32, streamOutput bool) { _, err := h.service.q.ChatModelByName(context.Background(), session.Model) if err != nil { apiErr := ErrResourceNotFound("Chat model: " + session.Model) apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } messages := simpleChatMessagesToMessages(simpleChatMessages) messages = append(messages, models.Message{ Role: "user", Content: newQuestion, }) model := h.chooseChatModel(session, messages) LLMAnswer, err := model.Stream(w, session, messages, "", false, streamOutput) if err != nil { RespondWithAPIError(w, WrapError(err, "Failed to generate answer")) return } ctx := context.Background() // Save to bot answer history historyParams := sqlc_queries.CreateBotAnswerHistoryParams{ BotUuid: snapshotUuid, UserID: userID, Prompt: newQuestion, Answer: LLMAnswer.Answer, Model: session.Model, TokensUsed: int32(len(LLMAnswer.Answer)) / 4, // Approximate token count } if _, err := h.service.q.CreateBotAnswerHistory(ctx, historyParams); err != nil { log.Printf("Failed to save bot answer history: %v", err) // Don't fail the request, just log the error } if !isTest(messages) { h.service.logChat(session, messages, LLMAnswer.Answer) } } // Helper function to convert SimpleChatMessage to Message func simpleChatMessagesToMessages(simpleChatMessages []SimpleChatMessage) []models.Message { messages := make([]models.Message, len(simpleChatMessages)) for i, scm := range simpleChatMessages { role := "user" if scm.Inversion { role = "assistant" } if i == 0 { role = "system" } messages[i] = models.Message{ Role: role, Content: scm.Text, } } return messages } func regenerateAnswer(h *ChatHandler, w http.ResponseWriter, ctx context.Context, chatSessionUuid string, chatUuid string, stream bool) { // Validate chat session chatSession, _, _, ok := h.validateChatSession(ctx, w, chatSessionUuid) if !ok { return } msgs, err := h.service.getAskMessages(*chatSession, chatUuid, true) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to get chat messages" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Store the request context so models can access it h.requestCtx = ctx model := h.chooseChatModel(*chatSession, msgs) LLMAnswer, err := model.Stream(w, *chatSession, msgs, chatUuid, true, stream) if err != nil { log.Printf("Error regenerating answer: %v", err) return } h.service.logChat(*chatSession, msgs, LLMAnswer.Answer) if err := h.service.UpdateChatMessageContent(ctx, chatUuid, LLMAnswer.Answer); err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to update message" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Generate suggested questions if explore mode is enabled if chatSession.ExploreMode { suggestedQuestions := h.service.generateSuggestedQuestions(LLMAnswer.Answer, msgs) if len(suggestedQuestions) > 0 { // Update the message with suggested questions in database questionsJSON, err := json.Marshal(suggestedQuestions) if err == nil { h.service.UpdateChatMessageSuggestions(ctx, chatUuid, questionsJSON) // Stream suggested questions to frontend if stream { h.sendSuggestedQuestionsStream(w, LLMAnswer.AnswerId, questionsJSON) } } } } } // GetRequestContext returns the current request context for streaming operations func (h *ChatHandler) GetRequestContext() context.Context { return h.requestCtx } func (h *ChatHandler) chooseChatModel(chat_session sqlc_queries.ChatSession, msgs []models.Message) ChatModel { model := chat_session.Model isTestChat := isTest(msgs) // If this is a test chat, return the test model immediately if isTestChat { return &TestChatModel{h: h} } // Get the chat model from database to access api_type field chatModel, err := GetChatModel(h.service.q, model) if err != nil { // Fallback to OpenAI if model not found in database return &OpenAIChatModel{h: h} } // Use api_type field from database instead of string prefix matching apiType := chatModel.ApiType completionModel := mapset.NewSet[string]() // completionModel.Add(openai.GPT3TextDavinci002) isCompletion := completionModel.Contains(model) var chatModelImpl ChatModel switch apiType { case "claude": chatModelImpl = &Claude3ChatModel{h: h} case "ollama": chatModelImpl = &OllamaChatModel{h: h} case "gemini": chatModelImpl = NewGeminiChatModel(h) case "custom": chatModelImpl = &CustomChatModel{h: h} case "openai": if isCompletion { chatModelImpl = &CompletionChatModel{h: h} } else { chatModelImpl = &OpenAIChatModel{h: h} } default: // Default to OpenAI for unknown api types chatModelImpl = &OpenAIChatModel{h: h} } return chatModelImpl } // isTest determines if the chat messages indicate this is a test scenario func isTest(msgs []models.Message) bool { if len(msgs) == 0 { return false } lastMsgs := msgs[len(msgs)-1] promptMsg := msgs[0] // Check if either first or last message contains test demo marker return (len(promptMsg.Content) >= TestPrefixLength && promptMsg.Content[:TestPrefixLength] == TestDemoPrefix) || (len(lastMsgs.Content) >= TestPrefixLength && lastMsgs.Content[:TestPrefixLength] == TestDemoPrefix) } func (h *ChatHandler) CheckModelAccess(w http.ResponseWriter, chatSessionUuid string, model string, userID int32) bool { chatModel, err := h.service.q.ChatModelByName(context.Background(), model) if err != nil { log.WithError(err).WithField("model", model).Error("Chat model not found") RespondWithAPIError(w, ErrResourceNotFound("chat model: "+model)) return true } log.Printf("%+v", chatModel) if !chatModel.EnablePerModeRatelimit { return false } ctx := context.Background() rate, err := h.service.q.RateLimiteByUserAndSessionUUID(ctx, sqlc_queries.RateLimiteByUserAndSessionUUIDParams{ Uuid: chatSessionUuid, UserID: userID, }) log.Printf("%+v", rate) if err != nil { if errors.Is(err, sql.ErrNoRows) { // If no rate limit is found, use a default value instead of returning an error log.Printf("No rate limit found for user %d and session %s, using default", userID, chatSessionUuid) return false } apiErr := WrapError(MapDatabaseError(err), "Failed to get rate limit") RespondWithAPIError(w, apiErr) return true } // get last model usage in 10min usage10Min, err := h.service.q.GetChatMessagesCountByUserAndModel(ctx, sqlc_queries.GetChatMessagesCountByUserAndModelParams{ UserID: userID, Model: rate.ChatModelName, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to get usage data" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return true } log.Printf("%+v", usage10Min) if int32(usage10Min) > rate.RateLimit { apiErr := ErrTooManyRequests apiErr.Message = fmt.Sprintf("Rate limit exceeded for %s", rate.ChatModelName) apiErr.Detail = fmt.Sprintf("Usage: %d, Limit: %d", usage10Min, rate.RateLimit) RespondWithAPIError(w, apiErr) return true } return false } ================================================ FILE: api/chat_main_service.go ================================================ package main import ( "bytes" "context" "encoding/json" "fmt" "log" "net/http" "os" "strings" "time" _ "embed" "github.com/rotisserie/eris" "github.com/samber/lo" openai "github.com/sashabaranov/go-openai" "github.com/swuecho/chat_backend/llm/gemini" models "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatService struct { q *sqlc_queries.Queries } //go:embed artifact_instruction.txt var artifactInstructionText string // NewChatService creates a new ChatService with database queries. func NewChatService(q *sqlc_queries.Queries) *ChatService { return &ChatService{q: q} } // loadArtifactInstruction loads the artifact instruction from file. // Returns the instruction content or an error if the file cannot be read. func loadArtifactInstruction() (string, error) { if artifactInstructionText == "" { return "", eris.New("artifact instruction text is empty") } return artifactInstructionText, nil } func appendInstructionToSystemMessage(msgs []models.Message, instruction string) { if instruction == "" || len(msgs) == 0 { return } systemMsgFound := false for i, msg := range msgs { if msg.Role == "system" { msgs[i].Content = msg.Content + "\n" + instruction msgs[i].SetTokenCount(int32(len(msgs[i].Content) / TokenEstimateRatio)) systemMsgFound = true break } } if !systemMsgFound { msgs[0].Content = msgs[0].Content + "\n" + instruction msgs[0].SetTokenCount(int32(len(msgs[0].Content) / TokenEstimateRatio)) } } // getAskMessages retrieves and processes chat messages for LLM requests. // It combines prompts and messages, applies length limits, and adds artifact instructions (unless explore mode is enabled). // Parameters: // - chatSession: The chat session containing configuration // - chatUuid: UUID for message identification (used in regenerate mode) // - regenerate: If true, excludes the target message from history // // Returns combined message array or error. func (s *ChatService) getAskMessages(chatSession sqlc_queries.ChatSession, chatUuid string, regenerate bool) ([]models.Message, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*RequestTimeoutSeconds) defer cancel() chatSessionUuid := chatSession.Uuid lastN := chatSession.MaxLength if chatSession.MaxLength == 0 { lastN = DefaultMaxLength } chat_prompts, err := s.q.GetChatPromptsBySessionUUID(ctx, chatSessionUuid) if err != nil { return nil, eris.Wrap(err, "fail to get prompt: ") } var chatMessages []sqlc_queries.ChatMessage if regenerate { chatMessages, err = s.q.GetLastNChatMessages(ctx, sqlc_queries.GetLastNChatMessagesParams{ ChatSessionUuid: chatSessionUuid, Uuid: chatUuid, Limit: lastN, }) } else { chatMessages, err = s.q.GetLatestMessagesBySessionUUID(ctx, sqlc_queries.GetLatestMessagesBySessionUUIDParams{ChatSessionUuid: chatSession.Uuid, Limit: lastN}) } if err != nil { return nil, eris.Wrap(err, "fail to get messages: ") } chatPromptMsgs := lo.Map(chat_prompts, func(m sqlc_queries.ChatPrompt, _ int) models.Message { msg := models.Message{Role: m.Role, Content: m.Content} msg.SetTokenCount(int32(m.TokenCount)) return msg }) chatMessageMsgs := lo.Map(chatMessages, func(m sqlc_queries.ChatMessage, _ int) models.Message { msg := models.Message{Role: m.Role, Content: m.Content} msg.SetTokenCount(int32(m.TokenCount)) return msg }) msgs := append(chatPromptMsgs, chatMessageMsgs...) // Add artifact instruction to system messages only if artifact mode is enabled if chatSession.ArtifactEnabled { artifactInstruction, err := loadArtifactInstruction() if err != nil { log.Printf("Warning: Failed to load artifact instruction: %v", err) artifactInstruction = "" // Use empty string if file can't be loaded } appendInstructionToSystemMessage(msgs, artifactInstruction) } return msgs, nil } // CreateChatPromptSimple creates a new chat prompt for a session. // This is typically used to start a new conversation with a system message. func (s *ChatService) CreateChatPromptSimple(ctx context.Context, chatSessionUuid string, newQuestion string, userID int32) (sqlc_queries.ChatPrompt, error) { tokenCount, err := getTokenCount(newQuestion) if err != nil { log.Printf("Warning: Failed to get token count for prompt: %v", err) tokenCount = len(newQuestion) / TokenEstimateRatio // Fallback estimate } chatPrompt, err := s.q.CreateChatPrompt(ctx, sqlc_queries.CreateChatPromptParams{ Uuid: NewUUID(), ChatSessionUuid: chatSessionUuid, Role: "system", Content: newQuestion, UserID: userID, CreatedBy: userID, UpdatedBy: userID, TokenCount: int32(tokenCount), }) return chatPrompt, err } // CreateChatMessageSimple creates a new chat message with optional summarization and artifact extraction. // Handles token counting, content summarization for long messages, and artifact parsing. // Parameters: // - ctx: Request context for cancellation // - sessionUuid, uuid: Message and session identifiers // - role: Message role (user/assistant/system) // - content, reasoningContent: Message content and reasoning (if any) // - model: LLM model name // - userId: User ID for ownership // - baseURL: API base URL for summarization // - is_summarize_mode: Whether to enable automatic summarization // // Returns created message or error. func (s *ChatService) CreateChatMessageSimple(ctx context.Context, sessionUuid, uuid, role, content, reasoningContent, model string, userId int32, baseURL string, is_summarize_mode bool) (sqlc_queries.ChatMessage, error) { numTokens, err := getTokenCount(content) if err != nil { log.Printf("Warning: Failed to get token count: %v", err) numTokens = len(content) / TokenEstimateRatio // Fallback estimate } summary := "" if is_summarize_mode && numTokens > SummarizeThreshold { log.Println("summarizing") summary = llm_summarize_with_timeout(baseURL, content) log.Println("summarizing: " + summary) } // Extract artifacts from content artifacts := extractArtifacts(content) artifactsJSON, err := json.Marshal(artifacts) if err != nil { log.Printf("Warning: Failed to marshal artifacts: %v", err) artifactsJSON = json.RawMessage([]byte("[]")) } chatMessage := sqlc_queries.CreateChatMessageParams{ ChatSessionUuid: sessionUuid, Uuid: uuid, Role: role, Content: content, ReasoningContent: reasoningContent, Model: model, UserID: userId, CreatedBy: userId, UpdatedBy: userId, LlmSummary: summary, TokenCount: int32(numTokens), Raw: json.RawMessage([]byte("{}")), Artifacts: artifactsJSON, SuggestedQuestions: json.RawMessage([]byte("[]")), } message, err := s.q.CreateChatMessage(ctx, chatMessage) if err != nil { return sqlc_queries.ChatMessage{}, eris.Wrap(err, "failed to create message ") } return message, nil } // CreateChatMessageWithSuggestedQuestions creates a chat message with optional suggested questions for explore mode func (s *ChatService) CreateChatMessageWithSuggestedQuestions(ctx context.Context, sessionUuid, uuid, role, content, reasoningContent, model string, userId int32, baseURL string, is_summarize_mode, exploreMode bool, messages []models.Message) (sqlc_queries.ChatMessage, error) { numTokens, err := getTokenCount(content) if err != nil { log.Printf("Warning: Failed to get token count: %v", err) numTokens = len(content) / TokenEstimateRatio // Fallback estimate } summary := "" if is_summarize_mode && numTokens > SummarizeThreshold { log.Println("summarizing") summary = llm_summarize_with_timeout(baseURL, content) log.Println("summarizing: " + summary) } // Extract artifacts from content artifacts := extractArtifacts(content) artifactsJSON, err := json.Marshal(artifacts) if err != nil { log.Printf("Warning: Failed to marshal artifacts: %v", err) artifactsJSON = json.RawMessage([]byte("[]")) } // Generate suggested questions if explore mode is enabled and role is assistant suggestedQuestions := json.RawMessage([]byte("[]")) if exploreMode && role == "assistant" && messages != nil { questions := s.generateSuggestedQuestions(content, messages) if questionsJSON, err := json.Marshal(questions); err == nil { suggestedQuestions = questionsJSON } else { log.Printf("Warning: Failed to marshal suggested questions: %v", err) } } chatMessage := sqlc_queries.CreateChatMessageParams{ ChatSessionUuid: sessionUuid, Uuid: uuid, Role: role, Content: content, ReasoningContent: reasoningContent, Model: model, UserID: userId, CreatedBy: userId, UpdatedBy: userId, LlmSummary: summary, TokenCount: int32(numTokens), Raw: json.RawMessage([]byte("{}")), Artifacts: artifactsJSON, SuggestedQuestions: suggestedQuestions, } message, err := s.q.CreateChatMessage(ctx, chatMessage) if err != nil { return sqlc_queries.ChatMessage{}, eris.Wrap(err, "failed to create message ") } return message, nil } // generateSuggestedQuestions generates follow-up questions based on the conversation context func (s *ChatService) generateSuggestedQuestions(content string, messages []models.Message) []string { // Create a simplified prompt to generate follow-up questions prompt := `Based on the following conversation, generate 3 thoughtful follow-up questions that would help explore the topic further. Return only the questions, one per line, without numbering or bullet points. Conversation context: ` // Add the last few messages for context (limit to avoid token overflow) contextMessages := messages if len(messages) > 6 { contextMessages = messages[len(messages)-6:] } for _, msg := range contextMessages { prompt += fmt.Sprintf("%s: %s\n", msg.Role, msg.Content) } prompt += fmt.Sprintf("assistant: %s\n\nGenerate 3 follow-up questions:", content) // Use the preferred models (deepseek-chat or gemini-2.0-flash) to generate suggestions questions := s.callLLMForSuggestions(prompt) // Parse the response into individual questions lines := strings.Split(strings.TrimSpace(questions), "\n") var result []string for _, line := range lines { line = strings.TrimSpace(line) if line != "" && len(result) < 3 { // Clean up any numbering or bullet points that might remain line = strings.TrimPrefix(line, "1. ") line = strings.TrimPrefix(line, "2. ") line = strings.TrimPrefix(line, "3. ") line = strings.TrimPrefix(line, "- ") line = strings.TrimPrefix(line, "• ") result = append(result, line) } } return result } // callLLMForSuggestions makes a simple API call to generate suggested questions func (s *ChatService) callLLMForSuggestions(prompt string) string { ctx := context.Background() // Get all models and find preferred models for suggestions allModels, err := s.q.ListChatModels(ctx) if err != nil { log.Printf("Warning: Failed to list models for suggestions: %v", err) return "" } // Filter for enabled models and prioritize deepseek-chat or gemini-2.0-flash var selectedModel sqlc_queries.ChatModel var foundPreferred bool // First pass: look for preferred models for _, model := range allModels { if !model.IsEnable { continue } modelNameLower := strings.ToLower(model.Name) if strings.Contains(modelNameLower, "deepseek-chat") || strings.Contains(modelNameLower, "gemini-2.0-flash") { selectedModel = model foundPreferred = true break } } // Second pass: fallback to any gemini or openai model if preferred not found if !foundPreferred { for _, model := range allModels { if !model.IsEnable { continue } apiType := strings.ToLower(model.ApiType) modelName := strings.ToLower(model.Name) // Prefer gemini models, then openai if apiType == "gemini" || (apiType == "openai" && strings.Contains(modelName, "gpt")) { selectedModel = model break } } } if selectedModel.ID == 0 { log.Printf("Warning: No suitable models available for suggestions") return "" } // Use different API calls based on model type apiType := strings.ToLower(selectedModel.ApiType) modelName := strings.ToLower(selectedModel.Name) if apiType == "gemini" || strings.Contains(modelName, "gemini") { return s.callGeminiForSuggestions(ctx, selectedModel, prompt) } else if strings.Contains(modelName, "deepseek") || apiType == "openai" { return s.callOpenAICompatibleForSuggestions(ctx, selectedModel, prompt) } log.Printf("Warning: Unsupported model type for suggestions: %s", selectedModel.ApiType) return "" } // callGeminiForSuggestions makes a Gemini API call for suggestions func (s *ChatService) callGeminiForSuggestions(ctx context.Context, model sqlc_queries.ChatModel, prompt string) string { // Validate API key apiKey := os.Getenv("GEMINI_API_KEY") if apiKey == "" { log.Printf("Warning: GEMINI_API_KEY environment variable not set") return "" } // Create messages for Gemini messages := []models.Message{ { Role: "user", Content: prompt, }, } // Generate Gemini payload payloadBytes, err := gemini.GenGemminPayload(messages, nil) if err != nil { log.Printf("Warning: Failed to generate Gemini payload for suggestions: %v", err) return "" } // Build URL url := gemini.BuildAPIURL(model.Name, false) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(payloadBytes)) if err != nil { log.Printf("Warning: Failed to create Gemini request for suggestions: %v", err) return "" } req.Header.Set("Content-Type", "application/json") // Make the API call with timeout ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() answer, err := gemini.HandleRegularResponse(http.Client{Timeout: 30 * time.Second}, req) if err != nil { log.Printf("Warning: Failed to get Gemini response for suggestions: %v", err) return "" } if answer == nil || answer.Answer == "" { log.Printf("Warning: Empty response from Gemini for suggestions") return "" } return answer.Answer } // callOpenAICompatibleForSuggestions makes an OpenAI-compatible API call for suggestions (including deepseek) func (s *ChatService) callOpenAICompatibleForSuggestions(ctx context.Context, model sqlc_queries.ChatModel, prompt string) string { // Generate OpenAI client configuration config, err := genOpenAIConfig(model) if err != nil { log.Printf("Warning: Failed to generate OpenAI configuration for suggestions: %v", err) return "" } client := openai.NewClientWithConfig(config) // Create a simple chat completion request for generating suggestions req := openai.ChatCompletionRequest{ Model: model.Name, Temperature: DefaultTemperature, Messages: []openai.ChatCompletionMessage{ { Role: "user", Content: prompt, }, }, MaxTokens: 200, // Keep suggestions concise } // Make the API call with timeout ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() resp, err := client.CreateChatCompletion(ctx, req) if err != nil { log.Printf("Warning: Failed to generate suggested questions with %s: %v", model.Name, err) return "" } if len(resp.Choices) == 0 { log.Printf("Warning: No response choices returned for suggested questions from %s", model.Name) return "" } return resp.Choices[0].Message.Content } // UpdateChatMessageContent updates the content of an existing chat message. // Recalculates token count for the updated content. func (s *ChatService) UpdateChatMessageContent(ctx context.Context, uuid, content string) error { // encode // num_tokens num_tokens, err := getTokenCount(content) if err != nil { log.Printf("Warning: Failed to get token count for update: %v", err) num_tokens = len(content) / TokenEstimateRatio // Fallback estimate } err = s.q.UpdateChatMessageContent(ctx, sqlc_queries.UpdateChatMessageContentParams{ Uuid: uuid, Content: content, TokenCount: int32(num_tokens), }) return err } // UpdateChatMessageSuggestions updates the suggested questions for a chat message func (s *ChatService) UpdateChatMessageSuggestions(ctx context.Context, uuid string, suggestedQuestions json.RawMessage) error { _, err := s.q.UpdateChatMessageSuggestions(ctx, sqlc_queries.UpdateChatMessageSuggestionsParams{ Uuid: uuid, SuggestedQuestions: suggestedQuestions, }) return err } // logChat creates a chat log entry for analytics and debugging. // Logs the session, messages, and LLM response for audit purposes. func (s *ChatService) logChat(chatSession sqlc_queries.ChatSession, msgs []models.Message, answerText string) { // log chat sessionRaw := chatSession.ToRawMessage() if sessionRaw == nil { log.Println("failed to marshal chat session") return } question, err := json.Marshal(msgs) if err != nil { log.Printf("Warning: Failed to marshal chat messages: %v", err) return // Skip logging if marshalling fails } answerRaw, err := json.Marshal(answerText) if err != nil { log.Printf("Warning: Failed to marshal answer: %v", err) return // Skip logging if marshalling fails } s.q.CreateChatLog(context.Background(), sqlc_queries.CreateChatLogParams{ Session: *sessionRaw, Question: question, Answer: answerRaw, }) } ================================================ FILE: api/chat_message_handler.go ================================================ package main import ( "database/sql" "encoding/json" "errors" "net/http" "strconv" "time" "github.com/gorilla/mux" "github.com/samber/lo" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatMessageHandler struct { service *ChatMessageService } func NewChatMessageHandler(sqlc_q *sqlc_queries.Queries) *ChatMessageHandler { chatMessageService := NewChatMessageService(sqlc_q) return &ChatMessageHandler{ service: chatMessageService, } } func (h *ChatMessageHandler) Register(router *mux.Router) { router.HandleFunc("/chat_messages", h.CreateChatMessage).Methods(http.MethodPost) router.HandleFunc("/chat_messages/{id}", h.GetChatMessageByID).Methods(http.MethodGet) router.HandleFunc("/chat_messages/{id}", h.UpdateChatMessage).Methods(http.MethodPut) router.HandleFunc("/chat_messages/{id}", h.DeleteChatMessage).Methods(http.MethodDelete) router.HandleFunc("/chat_messages", h.GetAllChatMessages).Methods(http.MethodGet) 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) } //type userIdContextKey string //const userIDKey = userIdContextKey("userID") func (h *ChatMessageHandler) CreateChatMessage(w http.ResponseWriter, r *http.Request) { var messageParams sqlc_queries.CreateChatMessageParams err := json.NewDecoder(r.Body).Decode(&messageParams) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } message, err := h.service.CreateChatMessage(r.Context(), messageParams) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to create chat message")) return } json.NewEncoder(w).Encode(message) } func (h *ChatMessageHandler) GetChatMessageByID(w http.ResponseWriter, r *http.Request) { idStr := mux.Vars(r)["id"] id, err := strconv.Atoi(idStr) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid chat message ID")) return } message, err := h.service.GetChatMessageByID(r.Context(), int32(id)) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get chat message")) return } json.NewEncoder(w).Encode(message) } func (h *ChatMessageHandler) UpdateChatMessage(w http.ResponseWriter, r *http.Request) { idStr := mux.Vars(r)["id"] id, err := strconv.Atoi(idStr) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid chat message ID")) return } var messageParams sqlc_queries.UpdateChatMessageParams err = json.NewDecoder(r.Body).Decode(&messageParams) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } messageParams.ID = int32(id) message, err := h.service.UpdateChatMessage(r.Context(), messageParams) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update chat message")) return } json.NewEncoder(w).Encode(message) } func (h *ChatMessageHandler) DeleteChatMessage(w http.ResponseWriter, r *http.Request) { idStr := mux.Vars(r)["id"] id, err := strconv.Atoi(idStr) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid chat message ID")) return } err = h.service.DeleteChatMessage(r.Context(), int32(id)) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to delete chat message")) return } w.WriteHeader(http.StatusOK) } func (h *ChatMessageHandler) GetAllChatMessages(w http.ResponseWriter, r *http.Request) { messages, err := h.service.GetAllChatMessages(r.Context()) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get chat messages")) return } json.NewEncoder(w).Encode(messages) } // GetChatMessageByUUID get chat message by uuid func (h *ChatMessageHandler) GetChatMessageByUUID(w http.ResponseWriter, r *http.Request) { uuidStr := mux.Vars(r)["uuid"] message, err := h.service.GetChatMessageByUUID(r.Context(), uuidStr) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get chat message")) return } json.NewEncoder(w).Encode(message) } // UpdateChatMessageByUUID update chat message by uuid func (h *ChatMessageHandler) UpdateChatMessageByUUID(w http.ResponseWriter, r *http.Request) { var simple_msg SimpleChatMessage err := json.NewDecoder(r.Body).Decode(&simple_msg) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } var messageParams sqlc_queries.UpdateChatMessageByUUIDParams messageParams.Uuid = simple_msg.Uuid messageParams.Content = simple_msg.Text tokenCount, _ := getTokenCount(simple_msg.Text) messageParams.TokenCount = int32(tokenCount) messageParams.IsPin = simple_msg.IsPin message, err := h.service.UpdateChatMessageByUUID(r.Context(), messageParams) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update chat message")) return } json.NewEncoder(w).Encode(message) } // DeleteChatMessageByUUID delete chat message by uuid func (h *ChatMessageHandler) DeleteChatMessageByUUID(w http.ResponseWriter, r *http.Request) { uuidStr := mux.Vars(r)["uuid"] err := h.service.DeleteChatMessageByUUID(r.Context(), uuidStr) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to delete chat message")) return } w.WriteHeader(http.StatusOK) } // GetChatMessagesBySessionUUID get chat messages by session uuid func (h *ChatMessageHandler) GetChatMessagesBySessionUUID(w http.ResponseWriter, r *http.Request) { uuidStr := mux.Vars(r)["uuid"] pageNum, err := strconv.Atoi(r.URL.Query().Get("page")) if err != nil { pageNum = 1 } pageSize, err := strconv.Atoi(r.URL.Query().Get("page_size")) if err != nil { pageSize = 200 } messages, err := h.service.GetChatMessagesBySessionUUID(r.Context(), uuidStr, int32(pageNum), int32(pageSize)) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get chat messages")) return } simple_msgs := lo.Map(messages, func(message sqlc_queries.ChatMessage, _ int) SimpleChatMessage { // Extract artifacts from database var artifacts []Artifact if message.Artifacts != nil { err := json.Unmarshal(message.Artifacts, &artifacts) if err != nil { // Log error but don't fail the request artifacts = []Artifact{} } } return SimpleChatMessage{ DateTime: message.UpdatedAt.Format(time.RFC3339), Text: message.Content, Inversion: message.Role != "user", Error: false, Loading: false, Artifacts: artifacts, } }) json.NewEncoder(w).Encode(simple_msgs) } // GetChatMessagesBySessionUUID get chat messages by session uuid func (h *ChatMessageHandler) GetChatHistoryBySessionUUID(w http.ResponseWriter, r *http.Request) { uuidStr := mux.Vars(r)["uuid"] pageNum, err := strconv.Atoi(r.URL.Query().Get("page")) if err != nil { pageNum = 1 } pageSize, err := strconv.Atoi(r.URL.Query().Get("page_size")) if err != nil { pageSize = 200 } simple_msgs, err := h.service.q.GetChatHistoryBySessionUUID(r.Context(), uuidStr, int32(pageNum), int32(pageSize)) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get chat history")) return } json.NewEncoder(w).Encode(simple_msgs) } // DeleteChatMessagesBySesionUUID delete chat messages by session uuid func (h *ChatMessageHandler) DeleteChatMessagesBySesionUUID(w http.ResponseWriter, r *http.Request) { uuidStr := mux.Vars(r)["uuid"] err := h.service.DeleteChatMessagesBySesionUUID(r.Context(), uuidStr) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to delete chat messages")) return } 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 { if errors.Is(err, sql.ErrNoRows) { RespondWithAPIError(w, ErrChatMessageNotFound.WithMessage("Message not found").WithDebugInfo(err.Error())) } else { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get message")) } return } // Only allow suggestions for assistant messages if message.Role != "assistant" { RespondWithAPIError(w, ErrValidationInvalidInput("Suggestions can only be generated for assistant messages")) return } // Get the session to check if explore mode is enabled session, err := h.service.q.GetChatSessionByUUID(r.Context(), message.ChatSessionUuid) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithAPIError(w, ErrChatSessionNotFound.WithMessage("Session not found").WithDebugInfo(err.Error())) } else { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get session")) } return } // Check if explore mode is enabled if !session.ExploreMode { RespondWithAPIError(w, ErrValidationInvalidInput("Suggestions are only available in explore mode")) 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 { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get conversation context")) 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 { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to generate suggestions", "no suggestions returned")) 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 { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to serialize suggestions", err.Error())) return } _, err = h.service.q.UpdateChatMessageSuggestions(r.Context(), sqlc_queries.UpdateChatMessageSuggestionsParams{ Uuid: messageUUID, SuggestedQuestions: suggestionsJSON, }) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update message with suggestions")) 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) } ================================================ FILE: api/chat_message_service.go ================================================ package main import ( "context" "encoding/json" "errors" "github.com/rotisserie/eris" "github.com/swuecho/chat_backend/ai" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatMessageService struct { q *sqlc_queries.Queries } // NewChatMessageService creates a new ChatMessageService. func NewChatMessageService(q *sqlc_queries.Queries) *ChatMessageService { return &ChatMessageService{q: q} } // CreateChatMessage creates a new chat message. func (s *ChatMessageService) CreateChatMessage(ctx context.Context, message_params sqlc_queries.CreateChatMessageParams) (sqlc_queries.ChatMessage, error) { message, err := s.q.CreateChatMessage(ctx, message_params) if err != nil { return sqlc_queries.ChatMessage{}, eris.Wrap(err, "failed to create message ") } return message, nil } // GetChatMessageByID returns a chat message by ID. func (s *ChatMessageService) GetChatMessageByID(ctx context.Context, id int32) (sqlc_queries.ChatMessage, error) { message, err := s.q.GetChatMessageByID(ctx, id) if err != nil { return sqlc_queries.ChatMessage{}, eris.Wrap(err, "failed to create message ") } return message, nil } // UpdateChatMessage updates an existing chat message. func (s *ChatMessageService) UpdateChatMessage(ctx context.Context, message_params sqlc_queries.UpdateChatMessageParams) (sqlc_queries.ChatMessage, error) { message_u, err := s.q.UpdateChatMessage(ctx, message_params) if err != nil { return sqlc_queries.ChatMessage{}, eris.Wrap(err, "failed to update message ") } return message_u, nil } // DeleteChatMessage deletes a chat message by ID. func (s *ChatMessageService) DeleteChatMessage(ctx context.Context, id int32) error { err := s.q.DeleteChatMessage(ctx, id) if err != nil { return eris.Wrap(err, "failed to delete message ") } return nil } // DeleteChatMessageByUUID deletes a chat message by uuid func (s *ChatMessageService) DeleteChatMessageByUUID(ctx context.Context, uuid string) error { err := s.q.DeleteChatMessageByUUID(ctx, uuid) if err != nil { return eris.Wrap(err, "failed to delete message ") } return nil } // GetAllChatMessages returns all chat messages. func (s *ChatMessageService) GetAllChatMessages(ctx context.Context) ([]sqlc_queries.ChatMessage, error) { messages, err := s.q.GetAllChatMessages(ctx) if err != nil { return nil, eris.Wrap(err, "failed to retrieve messages ") } return messages, nil } func (s *ChatMessageService) GetLatestMessagesBySessionID(ctx context.Context, chatSessionUuid string, limit int32) ([]sqlc_queries.ChatMessage, error) { params := sqlc_queries.GetLatestMessagesBySessionUUIDParams{ChatSessionUuid: chatSessionUuid, Limit: limit} msgs, err := s.q.GetLatestMessagesBySessionUUID(ctx, params) if err != nil { return []sqlc_queries.ChatMessage{}, err } return msgs, nil } func (s *ChatMessageService) GetFirstMessageBySessionUUID(ctx context.Context, chatSessionUuid string) (sqlc_queries.ChatMessage, error) { msg, err := s.q.GetFirstMessageBySessionUUID(ctx, chatSessionUuid) if err != nil { return sqlc_queries.ChatMessage{}, err } return msg, nil } func (s *ChatMessageService) AddMessage(ctx context.Context, chatSessionUuid string, uuid string, role ai.Role, content string, raw []byte) (sqlc_queries.ChatMessage, error) { params := sqlc_queries.CreateChatMessageParams{ ChatSessionUuid: chatSessionUuid, Uuid: uuid, Role: role.String(), Content: content, Raw: json.RawMessage(raw), } msg, err := s.q.CreateChatMessage(ctx, params) if err != nil { return sqlc_queries.ChatMessage{}, err } return msg, nil } // GetChatMessageByUUID returns a chat message by ID. func (s *ChatMessageService) GetChatMessageByUUID(ctx context.Context, uuid string) (sqlc_queries.ChatMessage, error) { message, err := s.q.GetChatMessageByUUID(ctx, uuid) if err != nil { return sqlc_queries.ChatMessage{}, errors.New("failed to retrieve message") } return message, nil } // UpdateChatMessageByUUID updates an existing chat message. func (s *ChatMessageService) UpdateChatMessageByUUID(ctx context.Context, message_params sqlc_queries.UpdateChatMessageByUUIDParams) (sqlc_queries.ChatMessage, error) { message_u, err := s.q.UpdateChatMessageByUUID(ctx, message_params) if err != nil { return sqlc_queries.ChatMessage{}, eris.Wrap(err, "failed to update message ") } return message_u, nil } // GetChatMessagesBySessionUUID returns a chat message by session uuid. func (s *ChatMessageService) GetChatMessagesBySessionUUID(ctx context.Context, uuid string, pageNum, pageSize int32) ([]sqlc_queries.ChatMessage, error) { param := sqlc_queries.GetChatMessagesBySessionUUIDParams{ Uuid: uuid, Offset: pageNum - 1, Limit: pageSize, } message, err := s.q.GetChatMessagesBySessionUUID(ctx, param) if err != nil { return []sqlc_queries.ChatMessage{}, eris.Wrap(err, "failed to retrieve message ") } return message, nil } // DeleteChatMessagesBySesionUUID deletes chat messages by session uuid. func (s *ChatMessageService) DeleteChatMessagesBySesionUUID(ctx context.Context, uuid string) error { err := s.q.DeleteChatMessagesBySesionUUID(ctx, uuid) return err } func (s *ChatMessageService) GetChatMessagesCount(ctx context.Context, userID int32) (int32, error) { count, err := s.q.GetChatMessagesCount(ctx, userID) if err != nil { return 0, err } return int32(count), nil } ================================================ FILE: api/chat_message_service_test.go ================================================ package main import ( "context" "database/sql" "encoding/json" "errors" "testing" "github.com/swuecho/chat_backend/sqlc_queries" ) func TestChatMessageService(t *testing.T) { // Create a new ChatMessageService with the test database connection q := sqlc_queries.New(db) service := NewChatMessageService(q) // Insert a new chat message into the database msg_params := sqlc_queries.CreateChatMessageParams{ ChatSessionUuid: "1", Uuid: "test-uuid-1", Role: "Test Role", Content: "Test Message", ReasoningContent: "", Model: "test-model", TokenCount: 100, Score: 0.5, UserID: 1, CreatedBy: 1, UpdatedBy: 1, LlmSummary: "", Raw: json.RawMessage([]byte("{}")), Artifacts: json.RawMessage([]byte("[]")), SuggestedQuestions: json.RawMessage([]byte("[]")), } msg, err := service.CreateChatMessage(context.Background(), msg_params) if err != nil { t.Fatalf("failed to create chat message: %v", err) } // Retrieve the inserted chat message from the database and check that it matches the expected values retrieved_msg, err := service.GetChatMessageByID(context.Background(), msg.ID) if err != nil { t.Fatalf("failed to retrieve chat message: %v", err) } if retrieved_msg.ID != msg.ID || retrieved_msg.ChatSessionUuid != msg.ChatSessionUuid || retrieved_msg.Role != msg.Role || retrieved_msg.Content != msg.Content || retrieved_msg.Score != msg.Score || retrieved_msg.UserID != msg.UserID || !retrieved_msg.CreatedAt.Equal(msg.CreatedAt) || !retrieved_msg.UpdatedAt.Equal(msg.UpdatedAt) || retrieved_msg.CreatedBy != msg.CreatedBy || retrieved_msg.UpdatedBy != msg.UpdatedBy { t.Error("retrieved chat message does not match expected values") } // Delete the chat prompt and check that it was deleted from the database if err := service.DeleteChatMessage(context.Background(), msg.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } _, err = service.GetChatMessageByID(context.Background(), msg.ID) if err == nil || !errors.Is(err, sql.ErrNoRows) { t.Error("expected error due to missing chat prompt, but got no error or different error") } } func TestGetChatMessagesBySessionID(t *testing.T) { // Create a new ChatMessageService with the test database connection q := sqlc_queries.New(db) service := NewChatMessageService(q) // Insert two chat messages into the database with different chat session IDs msg1_params := sqlc_queries.CreateChatMessageParams{ ChatSessionUuid: "1", Uuid: "test-uuid-1", Role: "Test Role 1", Content: "Test Message 1", ReasoningContent: "", Model: "test-model", TokenCount: 100, Score: 0.5, UserID: 1, CreatedBy: 1, UpdatedBy: 1, LlmSummary: "", Raw: json.RawMessage([]byte("{}")), Artifacts: json.RawMessage([]byte("[]")), SuggestedQuestions: json.RawMessage([]byte("[]")), } msg1, err := service.CreateChatMessage(context.Background(), msg1_params) if err != nil { t.Fatalf("failed to create chat message: %v", err) } msg2_params := sqlc_queries.CreateChatMessageParams{ ChatSessionUuid: "2", Uuid: "test-uuid-2", Role: "Test Role 2", Content: "Test Message 2", ReasoningContent: "", Model: "test-model", TokenCount: 100, Score: 0.75, UserID: 2, CreatedBy: 2, UpdatedBy: 2, LlmSummary: "", Raw: json.RawMessage([]byte("{}")), Artifacts: json.RawMessage([]byte("[]")), SuggestedQuestions: json.RawMessage([]byte("[]")), } msg2, err := service.CreateChatMessage(context.Background(), msg2_params) if err != nil { t.Fatalf("failed to create chat message: %v", err) } // Retrieve chat messages by chat session ID and check that they match the expected values // skip because of there is no chatSession with uuid "1" avaialble // chatSessionID := "1" // msgs, err := service.GetChatMessagesBySessionUUID(context.Background(), chatSessionID, 1, 10) // if err != nil { // t.Fatalf("failed to retrieve chat messages: %v", err) // } // if len(msgs) != 1 { // t.Errorf("expected 1 chat message, but got %d", len(msgs)) // } // if msgs[0].ChatSessionUuid != msg1.ChatSessionUuid || msgs[0].Role != msg1.Role || msgs[0].Content != msg1.Content || // msgs[0].Score != msg1.Score || msgs[0].UserID != msg1.UserID { // t.Error("retrieved chat messages do not match expected values") // } // Delete the chat prompt and check that it was deleted from the database if err := service.DeleteChatMessage(context.Background(), msg1.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } // Delete the chat prompt and check that it was deleted from the database if err := service.DeleteChatMessage(context.Background(), msg2.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } _, err = service.GetChatMessageByID(context.Background(), msg1.ID) if err == nil || !errors.Is(err, sql.ErrNoRows) { t.Error("expected error due to missing chat prompt, but got no error or different error") } _, err = service.GetChatMessageByID(context.Background(), msg2.ID) if err == nil || !errors.Is(err, sql.ErrNoRows) { t.Error("expected error due to missing chat prompt, but got no error or different error") } } ================================================ FILE: api/chat_model_handler.go ================================================ package main import ( "encoding/json" "net/http" "strconv" "time" "github.com/gorilla/mux" "github.com/samber/lo" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatModelHandler struct { db *sqlc_queries.Queries } func NewChatModelHandler(db *sqlc_queries.Queries) *ChatModelHandler { return &ChatModelHandler{ db: db, } } func (h *ChatModelHandler) Register(r *mux.Router) { // Assuming db is an instance of the SQLC generated DB struct //handler := NewChatModelHandler(db) // r := mux.NewRouter() // TODO: user can read, remove user_id field from the response r.HandleFunc("/chat_model", h.ListSystemChatModels).Methods("GET") r.HandleFunc("/chat_model/default", h.GetDefaultChatModel).Methods("GET") r.HandleFunc("/chat_model/{id}", h.ChatModelByID).Methods("GET") // create delete update self's chat model r.HandleFunc("/chat_model", h.CreateChatModel).Methods("POST") r.HandleFunc("/chat_model/{id}", h.UpdateChatModel).Methods("PUT") r.HandleFunc("/chat_model/{id}", h.DeleteChatModel).Methods("DELETE") } func (h *ChatModelHandler) ListSystemChatModels(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ChatModels, err := h.db.ListSystemChatModels(ctx) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to list chat models" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } latestUsageTimeOfModels, err := h.db.GetLatestUsageTimeOfModel(ctx, "30 days") if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to get model usage data" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // create a map of model id to usage time usageTimeMap := make(map[string]sqlc_queries.GetLatestUsageTimeOfModelRow) for _, usageTime := range latestUsageTimeOfModels { usageTimeMap[usageTime.Model] = usageTime } // create a ChatModelWithUsage struct type ChatModelWithUsage struct { sqlc_queries.ChatModel LastUsageTime time.Time `json:"lastUsageTime,omitempty"` MessageCount int64 `json:"messageCount"` } // merge ChatModels and usageTimeMap with pre-allocated slice chatModelsWithUsage := lo.Map(ChatModels, func(model sqlc_queries.ChatModel, _ int) ChatModelWithUsage { usage := usageTimeMap[model.Name] return ChatModelWithUsage{ ChatModel: model, LastUsageTime: usage.LatestMessageTime, MessageCount: usage.MessageCount, } }) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(chatModelsWithUsage) } func (h *ChatModelHandler) ChatModelByID(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) ctx := r.Context() id, err := strconv.Atoi(vars["id"]) if err != nil { apiErr := ErrValidationInvalidInput("Invalid chat model ID") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } ChatModel, err := h.db.ChatModelByID(ctx, int32(id)) if err != nil { apiErr := ErrResourceNotFound("Chat model") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ChatModel) } func (h *ChatModelHandler) CreateChatModel(w http.ResponseWriter, r *http.Request) { userID, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } var input struct { Name string `json:"name"` Label string `json:"label"` IsDefault bool `json:"isDefault"` URL string `json:"url"` ApiAuthHeader string `json:"apiAuthHeader"` ApiAuthKey string `json:"apiAuthKey"` EnablePerModeRatelimit bool `json:"enablePerModeRatelimit"` ApiType string `json:"apiType"` } err = json.NewDecoder(r.Body).Decode(&input) if err != nil { apiErr := ErrValidationInvalidInput("Failed to parse request body") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Set default api_type if not provided apiType := input.ApiType if apiType == "" { apiType = "openai" // default api type } // Validate api_type validApiTypes := map[string]bool{ "openai": true, "claude": true, "gemini": true, "ollama": true, "custom": true, } if !validApiTypes[apiType] { apiErr := ErrValidationInvalidInput("Invalid API type. Valid types are: openai, claude, gemini, ollama, custom") RespondWithAPIError(w, apiErr) return } ChatModel, err := h.db.CreateChatModel(r.Context(), sqlc_queries.CreateChatModelParams{ Name: input.Name, Label: input.Label, IsDefault: input.IsDefault, Url: input.URL, ApiAuthHeader: input.ApiAuthHeader, ApiAuthKey: input.ApiAuthKey, UserID: userID, EnablePerModeRatelimit: input.EnablePerModeRatelimit, MaxToken: 4096, // default max token DefaultToken: 2048, // default token OrderNumber: 0, // default order HttpTimeOut: 120, // default timeout ApiType: apiType, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to create chat model" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(ChatModel) } func (h *ChatModelHandler) UpdateChatModel(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { apiErr := ErrValidationInvalidInput("Invalid chat model ID") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } userID, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } var input struct { Name string `json:"name"` Label string `json:"label"` IsDefault bool `json:"isDefault"` URL string `json:"url"` ApiAuthHeader string `json:"apiAuthHeader"` ApiAuthKey string `json:"apiAuthKey"` EnablePerModeRatelimit bool `json:"enablePerModeRatelimit"` OrderNumber int32 `json:"orderNumber"` DefaultToken int32 `json:"defaultToken"` MaxToken int32 `json:"maxToken"` HttpTimeOut int32 `json:"httpTimeOut"` IsEnable bool `json:"isEnable"` ApiType string `json:"apiType"` } err = json.NewDecoder(r.Body).Decode(&input) if err != nil { apiErr := ErrValidationInvalidInput("Failed to parse request body") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Set default api_type if not provided apiType := input.ApiType if apiType == "" { apiType = "openai" // default api type } // Validate api_type validApiTypes := map[string]bool{ "openai": true, "claude": true, "gemini": true, "ollama": true, "custom": true, } if !validApiTypes[apiType] { apiErr := ErrValidationInvalidInput("Invalid API type. Valid types are: openai, claude, gemini, ollama, custom") RespondWithAPIError(w, apiErr) return } ChatModel, err := h.db.UpdateChatModel(r.Context(), sqlc_queries.UpdateChatModelParams{ ID: int32(id), Name: input.Name, Label: input.Label, IsDefault: input.IsDefault, Url: input.URL, ApiAuthHeader: input.ApiAuthHeader, ApiAuthKey: input.ApiAuthKey, UserID: userID, EnablePerModeRatelimit: input.EnablePerModeRatelimit, OrderNumber: input.OrderNumber, DefaultToken: input.DefaultToken, MaxToken: input.MaxToken, HttpTimeOut: input.HttpTimeOut, IsEnable: input.IsEnable, ApiType: apiType, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to update chat model" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ChatModel) } func (h *ChatModelHandler) DeleteChatModel(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { apiErr := ErrValidationInvalidInput("Invalid chat model ID") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } userID, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } err = h.db.DeleteChatModel(r.Context(), sqlc_queries.DeleteChatModelParams{ ID: int32(id), UserID: userID, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to delete chat model" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusOK) } func (h *ChatModelHandler) GetDefaultChatModel(w http.ResponseWriter, r *http.Request) { ChatModel, err := h.db.GetDefaultChatModel(r.Context()) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to retrieve default chat model" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ChatModel) } ================================================ FILE: api/chat_model_handler_test.go ================================================ package main import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/google/go-cmp/cmp/cmpopts" "github.com/gorilla/mux" "github.com/samber/lo" "github.com/swuecho/chat_backend/sqlc_queries" "gotest.tools/v3/assert" ) func createTwoChatModel(q *sqlc_queries.Queries) (sqlc_queries.AuthUser, []sqlc_queries.ChatModel) { // add a system user admin, err := q.CreateAuthUser(context.Background(), sqlc_queries.CreateAuthUserParams{ Email: "admin@a.com", Username: "test", Password: "test", IsSuperuser: true, }) if err != nil { fmt.Printf("Error creating test data: %s", err.Error()) } expectedResults := []sqlc_queries.ChatModel{ { Name: "Test API 1", Label: "Test Label 1", IsDefault: false, Url: "http://test.url.com", ApiAuthHeader: "Authorization", ApiAuthKey: "TestKey1", UserID: admin.ID, }, { Name: "Test API 2", Label: "Test Label 2", IsDefault: false, Url: "http://test.url2.com", ApiAuthHeader: "Authorization", ApiAuthKey: "TestKey2", UserID: admin.ID, }, } for _, api := range expectedResults { _, err := q.CreateChatModel(context.Background(), sqlc_queries.CreateChatModelParams{ Name: api.Name, Label: api.Label, IsDefault: api.IsDefault, Url: api.Url, ApiAuthHeader: api.ApiAuthHeader, ApiAuthKey: api.ApiAuthKey, UserID: api.UserID, }) if err != nil { fmt.Printf("Error creating test data: %s", err.Error()) } } return admin, expectedResults } func clearChatModelsIfExists(q *sqlc_queries.Queries) { defaultApis, _ := q.ListChatModels(context.Background()) for _, api := range defaultApis { q.DeleteChatModel(context.Background(), sqlc_queries.DeleteChatModelParams{ ID: api.ID, UserID: api.UserID, }) } } func unmarshalResponseToChatModel(t *testing.T, rr *httptest.ResponseRecorder) []sqlc_queries.ChatModel { // read the response body // unmarshal the response body into a list of ChatModel var results []sqlc_queries.ChatModel err := json.NewDecoder(rr.Body).Decode(&results) assert.NilError(t, err) return results } // the code below do db update directly in instead of using handler, please change to use handler func TestChatModelTest(t *testing.T) { q := sqlc_queries.New(db) h := NewChatModelHandler(q) // create a new ChatModelHandler instance for testing router := mux.NewRouter() h.Register(router) // delete all existing chat APIs clearChatModelsIfExists(q) // Now let's create our expected results. Create two results and insert them into the database using the queries. admin, expectedResults := createTwoChatModel(q) // ensure that we get an array of two chat APIs in the response body // ensure the returned values are what we expect them to be results := checkGetModels(t, router, expectedResults) // Now lets update the the first element of our expected results array and call PUT on the endpoint // Create an HTTP request so we can simulate a PUT with the payload // ensure the new values are returned and were also updated in the database firstRecordID := results[0].ID updateFirstRecord(t, router, firstRecordID, admin, expectedResults[0]) // delete first model deleteReq, _ := http.NewRequest("DELETE", fmt.Sprintf("/chat_model/%d", firstRecordID), nil) deleteReq = deleteReq.WithContext(getContextWithUser(int(admin.ID))) deleteRR := httptest.NewRecorder() router.ServeHTTP(deleteRR, deleteReq) assert.Equal(t, deleteRR.Code, http.StatusOK) // check only one model left req, _ := http.NewRequest("GET", "/chat_model", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) // ensure that we get an array of one chat API in the response body results = unmarshalResponseToChatModel(t, rr) assert.Equal(t, len(results), 1) assert.Equal(t, results[0].Name, "Test API 1") // delete the last model deleteRequest, _ := http.NewRequest("DELETE", fmt.Sprintf("/chat_model/%d", results[0].ID), nil) contextWithUser := getContextWithUser(int(admin.ID)) deleteRequest = deleteRequest.WithContext(contextWithUser) deleteResponseRecorder := httptest.NewRecorder() router.ServeHTTP(deleteResponseRecorder, deleteRequest) assert.Equal(t, deleteResponseRecorder.Code, http.StatusOK) // check no models left getRequest, _ := http.NewRequest("GET", "/chat_model", nil) // Create a ResponseRecorder to record the response getResponseRecorder := httptest.NewRecorder() router.ServeHTTP(getResponseRecorder, getRequest) results = unmarshalResponseToChatModel(t, getResponseRecorder) assert.Equal(t, len(results), 0) } func checkGetModels(t *testing.T, router *mux.Router, expectedResults []sqlc_queries.ChatModel) []sqlc_queries.ChatModel { req, _ := http.NewRequest("GET", "/chat_model", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) assert.Equal(t, rr.Code, http.StatusOK) var results []sqlc_queries.ChatModel err := json.NewDecoder(rr.Body).Decode(&results) if err != nil { t.Errorf("error parsing response body: %s", err.Error()) } assert.Equal(t, len(results), 2) assert.DeepEqual(t, lo.Reverse(expectedResults), results, cmpopts.IgnoreFields(sqlc_queries.ChatModel{}, "ID", "IsEnable")) return results } func updateFirstRecord(t *testing.T, router *mux.Router, chatModelID int32, admin sqlc_queries.AuthUser, rec sqlc_queries.ChatModel) { rec.Name = "Test API 1 Updated" rec.Label = "Test Label 1 Updated" updateBytes, err := json.Marshal(rec) if err != nil { t.Errorf("Error marshaling update payload: %s", err.Error()) } updateReq, _ := http.NewRequest("PUT", fmt.Sprintf("/chat_model/%d", chatModelID), bytes.NewBuffer(updateBytes)) updateReq = updateReq.WithContext(getContextWithUser(int(admin.ID))) updateRR := httptest.NewRecorder() router.ServeHTTP(updateRR, updateReq) assert.Equal(t, updateRR.Code, http.StatusOK) var updatedResult sqlc_queries.ChatModel err = json.Unmarshal(updateRR.Body.Bytes(), &updatedResult) if err != nil { t.Errorf("Error parsing response body: %s", err.Error()) } assert.Equal(t, rec.Name, updatedResult.Name) assert.Equal(t, rec.Label, updatedResult.Label) } ================================================ FILE: api/chat_model_privilege_handler.go ================================================ package main import ( "database/sql" "encoding/json" "errors" "fmt" "log" "net/http" "strconv" "github.com/gorilla/mux" "github.com/samber/lo" "github.com/swuecho/chat_backend/sqlc_queries" ) // UserChatModelPrivilegeHandler handles requests related to user chat model privileges type UserChatModelPrivilegeHandler struct { db *sqlc_queries.Queries } // NewUserChatModelPrivilegeHandler creates a new handler instance func NewUserChatModelPrivilegeHandler(db *sqlc_queries.Queries) *UserChatModelPrivilegeHandler { return &UserChatModelPrivilegeHandler{ db: db, } } // Register sets up the handler routes func (h *UserChatModelPrivilegeHandler) Register(r *mux.Router) { r.HandleFunc("/admin/user_chat_model_privilege", h.ListUserChatModelPrivileges).Methods(http.MethodGet) r.HandleFunc("/admin/user_chat_model_privilege", h.CreateUserChatModelPrivilege).Methods(http.MethodPost) r.HandleFunc("/admin/user_chat_model_privilege/{id}", h.DeleteUserChatModelPrivilege).Methods(http.MethodDelete) r.HandleFunc("/admin/user_chat_model_privilege/{id}", h.UpdateUserChatModelPrivilege).Methods(http.MethodPut) } type ChatModelPrivilege struct { ID int32 `json:"id"` FullName string `json:"fullName"` UserEmail string `json:"userEmail"` ChatModelName string `json:"chatModelName"` RateLimit int32 `json:"rateLimit"` } // ListUserChatModelPrivileges handles GET requests to list all user chat model privileges func (h *UserChatModelPrivilegeHandler) ListUserChatModelPrivileges(w http.ResponseWriter, r *http.Request) { // TODO: check user is super_user userChatModelRows, err := h.db.ListUserChatModelPrivilegesRateLimit(r.Context()) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to list user chat model privileges")) return } log.Printf("Listing user chat model privileges") output := lo.Map(userChatModelRows, func(r sqlc_queries.ListUserChatModelPrivilegesRateLimitRow, idx int) ChatModelPrivilege { return ChatModelPrivilege{ ID: r.ID, FullName: r.FullName, UserEmail: r.UserEmail, ChatModelName: r.ChatModelName, RateLimit: r.RateLimit, } }) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(output) } func (h *UserChatModelPrivilegeHandler) UserChatModelPrivilegeByID(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid user chat model privilege ID")) return } userChatModelPrivilege, err := h.db.UserChatModelPrivilegeByID(r.Context(), int32(id)) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to get user chat model privilege")) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(userChatModelPrivilege) } // CreateUserChatModelPrivilege handles POST requests to create a new user chat model privilege func (h *UserChatModelPrivilegeHandler) CreateUserChatModelPrivilege(w http.ResponseWriter, r *http.Request) { var input ChatModelPrivilege err := json.NewDecoder(r.Body).Decode(&input) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("failed to parse request body")) return } // Validate input if input.UserEmail == "" { RespondWithAPIError(w, ErrValidationInvalidInput("user email is required")) return } if input.ChatModelName == "" { RespondWithAPIError(w, ErrValidationInvalidInput("chat model name is required")) return } if input.RateLimit <= 0 { RespondWithAPIError(w, ErrValidationInvalidInput("rate limit must be positive").WithMessage( fmt.Sprintf("invalid rate limit: %d", input.RateLimit))) return } log.Printf("Creating chat model privilege for user %s with model %s", input.UserEmail, input.ChatModelName) user, err := h.db.GetAuthUserByEmail(r.Context(), input.UserEmail) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithAPIError(w, ErrResourceNotFound("user").WithMessage( fmt.Sprintf("user with email %s not found", input.UserEmail))) } else { RespondWithAPIError(w, WrapError(err, "failed to get user by email")) } return } chatModel, err := h.db.ChatModelByName(r.Context(), input.ChatModelName) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithAPIError(w, ErrChatModelNotFound.WithMessage(fmt.Sprintf("chat model %s not found", input.ChatModelName))) } else { RespondWithAPIError(w, WrapError(err, "failed to get chat model")) } return } userChatModelPrivilege, err := h.db.CreateUserChatModelPrivilege(r.Context(), sqlc_queries.CreateUserChatModelPrivilegeParams{ UserID: user.ID, ChatModelID: chatModel.ID, RateLimit: input.RateLimit, CreatedBy: user.ID, UpdatedBy: user.ID, }) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithAPIError(w, ErrResourceNotFound("chat model privilege")) } else { RespondWithAPIError(w, WrapError(err, "failed to create user chat model privilege")) } return } output := ChatModelPrivilege{ ID: userChatModelPrivilege.ID, UserEmail: user.Email, ChatModelName: chatModel.Name, RateLimit: userChatModelPrivilege.RateLimit, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(output) } // UpdateUserChatModelPrivilege handles PUT requests to update a user chat model privilege func (h *UserChatModelPrivilegeHandler) UpdateUserChatModelPrivilege(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid user chat model privilege ID")) return } userID, err := getUserID(r.Context()) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } var input ChatModelPrivilege err = json.NewDecoder(r.Body).Decode(&input) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("failed to parse request body")) return } // Validate input if input.RateLimit <= 0 { RespondWithAPIError(w, ErrValidationInvalidInput("rate limit must be positive")) return } log.Printf("Updating chat model privilege %d for user %d", id, userID) userChatModelPrivilege, err := h.db.UpdateUserChatModelPrivilege(r.Context(), sqlc_queries.UpdateUserChatModelPrivilegeParams{ ID: int32(id), RateLimit: input.RateLimit, UpdatedBy: userID, }) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithAPIError(w, ErrResourceNotFound("chat model privilege")) } else { RespondWithAPIError(w, WrapError(err, "failed to update user chat model privilege")) } return } output := ChatModelPrivilege{ ID: userChatModelPrivilege.ID, UserEmail: input.UserEmail, ChatModelName: input.ChatModelName, RateLimit: userChatModelPrivilege.RateLimit, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(output) } func (h *UserChatModelPrivilegeHandler) DeleteUserChatModelPrivilege(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid user chat model privilege ID")) return } err = h.db.DeleteUserChatModelPrivilege(r.Context(), int32(id)) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithAPIError(w, ErrResourceNotFound("chat model privilege")) } else { RespondWithAPIError(w, WrapError(err, "failed to delete user chat model privilege")) } return } w.WriteHeader(http.StatusNoContent) } func (h *UserChatModelPrivilegeHandler) UserChatModelPrivilegeByUserAndModelID(w http.ResponseWriter, r *http.Request) { _, err := getUserID(r.Context()) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } var input struct { UserID int32 ChatModelID int32 } err = json.NewDecoder(r.Body).Decode(&input) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("failed to parse request body")) return } userChatModelPrivilege, err := h.db.UserChatModelPrivilegeByUserAndModelID(r.Context(), sqlc_queries.UserChatModelPrivilegeByUserAndModelIDParams{ UserID: input.UserID, ChatModelID: input.ChatModelID, }) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to get user chat model privilege")) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(userChatModelPrivilege) } func (h *UserChatModelPrivilegeHandler) ListUserChatModelPrivilegesByUserID(w http.ResponseWriter, r *http.Request) { userID, err := getUserID(r.Context()) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } privileges, err := h.db.ListUserChatModelPrivilegesByUserID(r.Context(), int32(userID)) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to list privileges for user")) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(privileges) } ================================================ FILE: api/chat_prompt_hander.go ================================================ package main import ( "database/sql" "encoding/json" "errors" "net/http" "strconv" "github.com/gorilla/mux" "github.com/jackc/pgconn" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatPromptHandler struct { service *ChatPromptService } func NewChatPromptHandler(sqlc_q *sqlc_queries.Queries) *ChatPromptHandler { promptService := NewChatPromptService(sqlc_q) return &ChatPromptHandler{ service: promptService, } } func (h *ChatPromptHandler) Register(router *mux.Router) { router.HandleFunc("/chat_prompts", h.CreateChatPrompt).Methods(http.MethodPost) router.HandleFunc("/chat_prompts/users", h.GetChatPromptsByUserID).Methods(http.MethodGet) router.HandleFunc("/chat_prompts/{id}", h.GetChatPromptByID).Methods(http.MethodGet) router.HandleFunc("/chat_prompts/{id}", h.UpdateChatPrompt).Methods(http.MethodPut) router.HandleFunc("/chat_prompts/{id}", h.DeleteChatPrompt).Methods(http.MethodDelete) router.HandleFunc("/chat_prompts", h.GetAllChatPrompts).Methods(http.MethodGet) router.HandleFunc("/uuid/chat_prompts/{uuid}", h.DeleteChatPromptByUUID).Methods(http.MethodDelete) router.HandleFunc("/uuid/chat_prompts/{uuid}", h.UpdateChatPromptByUUID).Methods(http.MethodPut) } func (h *ChatPromptHandler) CreateChatPrompt(w http.ResponseWriter, r *http.Request) { var promptParams sqlc_queries.CreateChatPromptParams err := json.NewDecoder(r.Body).Decode(&promptParams) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } userID, err := getUserID(r.Context()) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithDebugInfo(err.Error())) return } // Always trust authenticated user identity over client-provided values. promptParams.UserID = userID promptParams.CreatedBy = userID promptParams.UpdatedBy = userID // Idempotent creation for session system prompt: // return existing prompt instead of inserting duplicates when concurrent // frontend/backend requests race on a fresh session. if promptParams.ChatSessionUuid != "" && promptParams.Role == "system" { existingPrompt, getErr := h.service.q.GetOneChatPromptBySessionUUID(r.Context(), promptParams.ChatSessionUuid) if getErr == nil { json.NewEncoder(w).Encode(existingPrompt) return } if !errors.Is(getErr, sql.ErrNoRows) { RespondWithAPIError(w, WrapError(MapDatabaseError(getErr), "Failed to check existing chat prompt")) return } } prompt, err := h.service.CreateChatPrompt(r.Context(), promptParams) if err != nil { // Handle race: another request inserted the same session system prompt // between our read check and insert attempt. var pgErr *pgconn.PgError if promptParams.ChatSessionUuid != "" && promptParams.Role == "system" && errors.As(err, &pgErr) && pgErr.Code == "23505" { existingPrompt, getErr := h.service.q.GetOneChatPromptBySessionUUID(r.Context(), promptParams.ChatSessionUuid) if getErr == nil { json.NewEncoder(w).Encode(existingPrompt) return } } RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to create chat prompt")) return } json.NewEncoder(w).Encode(prompt) } func (h *ChatPromptHandler) GetChatPromptByID(w http.ResponseWriter, r *http.Request) { idStr := mux.Vars(r)["id"] id, err := strconv.Atoi(idStr) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid chat prompt ID")) return } prompt, err := h.service.GetChatPromptByID(r.Context(), int32(id)) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get chat prompt")) return } json.NewEncoder(w).Encode(prompt) } func (h *ChatPromptHandler) UpdateChatPrompt(w http.ResponseWriter, r *http.Request) { idStr := mux.Vars(r)["id"] id, err := strconv.Atoi(idStr) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid chat prompt ID")) return } var promptParams sqlc_queries.UpdateChatPromptParams err = json.NewDecoder(r.Body).Decode(&promptParams) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } promptParams.ID = int32(id) prompt, err := h.service.UpdateChatPrompt(r.Context(), promptParams) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update chat prompt")) return } json.NewEncoder(w).Encode(prompt) } func (h *ChatPromptHandler) DeleteChatPrompt(w http.ResponseWriter, r *http.Request) { idStr := mux.Vars(r)["id"] id, err := strconv.Atoi(idStr) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid chat prompt ID")) return } err = h.service.DeleteChatPrompt(r.Context(), int32(id)) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to delete chat prompt")) return } w.WriteHeader(http.StatusOK) } func (h *ChatPromptHandler) GetAllChatPrompts(w http.ResponseWriter, r *http.Request) { prompts, err := h.service.GetAllChatPrompts(r.Context()) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get chat prompts")) return } json.NewEncoder(w).Encode(prompts) } func (h *ChatPromptHandler) GetChatPromptsByUserID(w http.ResponseWriter, r *http.Request) { idStr := mux.Vars(r)["id"] id, err := strconv.Atoi(idStr) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid user ID")) return } prompts, err := h.service.GetChatPromptsByUserID(r.Context(), int32(id)) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to get chat prompts by user")) return } json.NewEncoder(w).Encode(prompts) } func (h *ChatPromptHandler) DeleteChatPromptByUUID(w http.ResponseWriter, r *http.Request) { idStr := mux.Vars(r)["uuid"] err := h.service.DeleteChatPromptByUUID(r.Context(), idStr) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to delete chat prompt")) return } w.WriteHeader(http.StatusOK) } func (h *ChatPromptHandler) UpdateChatPromptByUUID(w http.ResponseWriter, r *http.Request) { var simple_msg SimpleChatMessage err := json.NewDecoder(r.Body).Decode(&simple_msg) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) return } prompt, err := h.service.UpdateChatPromptByUUID(r.Context(), simple_msg.Uuid, simple_msg.Text) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to update chat prompt")) return } json.NewEncoder(w).Encode(prompt) } ================================================ FILE: api/chat_prompt_service.go ================================================ package main import ( "context" "errors" "github.com/rotisserie/eris" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatPromptService struct { q *sqlc_queries.Queries } // NewChatPromptService creates a new ChatPromptService. func NewChatPromptService(q *sqlc_queries.Queries) *ChatPromptService { return &ChatPromptService{q: q} } // CreateChatPrompt creates a new chat prompt. func (s *ChatPromptService) CreateChatPrompt(ctx context.Context, prompt_params sqlc_queries.CreateChatPromptParams) (sqlc_queries.ChatPrompt, error) { prompt, err := s.q.CreateChatPrompt(ctx, prompt_params) if err != nil { return sqlc_queries.ChatPrompt{}, eris.Wrap(err, "failed to create prompt: ") } return prompt, nil } func (s *ChatPromptService) CreateChatPromptWithUUID(ctx context.Context, uuid string, role, content string) (sqlc_queries.ChatPrompt, error) { params := sqlc_queries.CreateChatPromptParams{ ChatSessionUuid: uuid, Role: role, Content: content, } prompt, err := s.q.CreateChatPrompt(ctx, params) return prompt, err } // GetChatPromptByID returns a chat prompt by ID. func (s *ChatPromptService) GetChatPromptByID(ctx context.Context, id int32) (sqlc_queries.ChatPrompt, error) { prompt, err := s.q.GetChatPromptByID(ctx, id) if err != nil { return sqlc_queries.ChatPrompt{}, eris.Wrap(err, "failed to create prompt: ") } return prompt, nil } // UpdateChatPrompt updates an existing chat prompt. func (s *ChatPromptService) UpdateChatPrompt(ctx context.Context, prompt_params sqlc_queries.UpdateChatPromptParams) (sqlc_queries.ChatPrompt, error) { prompt_u, err := s.q.UpdateChatPrompt(ctx, prompt_params) if err != nil { return sqlc_queries.ChatPrompt{}, errors.New("failed to update prompt") } return prompt_u, nil } // DeleteChatPrompt deletes a chat prompt by ID. func (s *ChatPromptService) DeleteChatPrompt(ctx context.Context, id int32) error { err := s.q.DeleteChatPrompt(ctx, id) if err != nil { return errors.New("failed to delete prompt") } return nil } // GetAllChatPrompts returns all chat prompts. func (s *ChatPromptService) GetAllChatPrompts(ctx context.Context) ([]sqlc_queries.ChatPrompt, error) { prompts, err := s.q.GetAllChatPrompts(ctx) if err != nil { return nil, errors.New("failed to retrieve prompts") } return prompts, nil } func (s *ChatPromptService) GetChatPromptsByUserID(ctx context.Context, userID int32) ([]sqlc_queries.ChatPrompt, error) { prompts, err := s.q.GetChatPromptsByUserID(ctx, userID) if err != nil { return nil, err } return prompts, nil } func (s *ChatPromptService) GetChatPromptsBySessionUUID(ctx context.Context, session_uuid string) ([]sqlc_queries.ChatPrompt, error) { prompts, err := s.q.GetChatPromptsBySessionUUID(ctx, session_uuid) if err != nil { return nil, err } return prompts, nil } // DeleteChatPromptByUUID func (s *ChatPromptService) DeleteChatPromptByUUID(ctx context.Context, uuid string) error { err := s.q.DeleteChatPromptByUUID(ctx, uuid) if err != nil { return err } return nil } // UpdateChatPromptByUUID func (s *ChatPromptService) UpdateChatPromptByUUID(ctx context.Context, uuid string, content string) (sqlc_queries.ChatPrompt, error) { tokenCount, _ := getTokenCount(content) params := sqlc_queries.UpdateChatPromptByUUIDParams{ Uuid: uuid, Content: content, TokenCount: int32(tokenCount), } prompt, err := s.q.UpdateChatPromptByUUID(ctx, params) return prompt, err } ================================================ FILE: api/chat_prompt_service_test.go ================================================ package main import ( "context" "database/sql" "errors" "fmt" "testing" _ "github.com/lib/pq" "github.com/swuecho/chat_backend/sqlc_queries" ) func TestChatPromptService(t *testing.T) { // Create a new ChatPromptService with the test database connection q := sqlc_queries.New(db) service := NewChatPromptService(q) // Insert a new chat prompt into the database prompt_params := sqlc_queries.CreateChatPromptParams{ChatSessionUuid: "Test Topic", Role: "Test Role", Content: "Test Content", UserID: 1} prompt, err := service.CreateChatPrompt(context.Background(), prompt_params) if err != nil { t.Fatalf("failed to create chat prompt: %v", err) } // Retrieve the chat prompt by ID and check that it matches the expected values retrievedPrompt, err := service.GetChatPromptByID(context.Background(), prompt.ID) if err != nil { t.Fatalf("failed to get chat prompt: %v", err) } if retrievedPrompt.ChatSessionUuid != prompt.ChatSessionUuid || retrievedPrompt.Role != prompt.Role || retrievedPrompt.Content != prompt.Content || retrievedPrompt.Score != prompt.Score || retrievedPrompt.UserID != prompt.UserID { t.Error("retrieved chat prompt does not match expected values") } // Update the chat prompt and check that it was updated in the database updated_params := sqlc_queries.UpdateChatPromptParams{ID: prompt.ID, ChatSessionUuid: "Updated Test Topic", Role: "Updated Test Role", Content: "Updated Test Content", Score: 0.75} if _, err := service.UpdateChatPrompt(context.Background(), updated_params); err != nil { t.Fatalf("failed to update chat prompt: %v", err) } retrievedPrompt, err = service.GetChatPromptByID(context.Background(), prompt.ID) if err != nil { t.Fatalf("failed to get chat prompt: %v", err) } if retrievedPrompt.ChatSessionUuid != updated_params.ChatSessionUuid || retrievedPrompt.Role != updated_params.Role || retrievedPrompt.Content != updated_params.Content || retrievedPrompt.Score != updated_params.Score { t.Error("retrieved chat prompt does not match expected values") } // Delete the chat prompt and check that it was deleted from the database if err := service.DeleteChatPrompt(context.Background(), prompt.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } _, err = service.GetChatPromptByID(context.Background(), prompt.ID) if err != nil && errors.Is(err, sql.ErrNoRows) { print("Chat prompt deleted successfully") } if err == nil || !errors.Is(err, sql.ErrNoRows) { t.Error("expected error due to missing chat prompt, but got no error or different error") } _, err = service.q.GetChatPromptsBySessionUUID(context.Background(), "12324") if err != nil { t.Error("expected error due to missing chat prompt, but got no error or different error") } } func TestGetAllChatPrompts(t *testing.T) { q := sqlc_queries.New(db) service := NewChatPromptService(q) // Insert two chat prompts into the database prompt1_params := sqlc_queries.CreateChatPromptParams{ChatSessionUuid: "Test Topic 1", Role: "Test Role 1", Content: "Test Content 1", UserID: 1} prompt1, err := service.CreateChatPrompt(context.Background(), prompt1_params) if err != nil { t.Fatalf("failed to create chat prompt: %v", err) } prompt2_params := sqlc_queries.CreateChatPromptParams{ChatSessionUuid: "Test Topic 2", Role: "Test Role 2", Content: "Test Content 2", UserID: 2} prompt2, err := service.CreateChatPrompt(context.Background(), prompt2_params) if err != nil { t.Fatalf("failed to create chat prompt: %v", err) } // Retrieve all chat prompts and check that they match the expected values prompts, err := service.GetAllChatPrompts(context.Background()) if err != nil { t.Fatalf("failed to retrieve chat prompts: %v", err) } if len(prompts) != 2 { t.Errorf("expected 2 chat prompts, but got %d", len(prompts)) } if prompts[0].ChatSessionUuid != prompt1.ChatSessionUuid || prompts[0].Role != prompt1.Role || prompts[0].Content != prompt1.Content || prompts[0].Score != prompt1.Score || prompts[0].UserID != prompt1.UserID || prompts[1].ChatSessionUuid != prompt2.ChatSessionUuid || prompts[1].Role != prompt2.Role || prompts[1].Content != prompt2.Content || prompts[1].Score != prompt2.Score || prompts[1].UserID != prompt2.UserID { t.Error("retrieved chat prompts do not match expected values") } // Delete the chat prompt and check that it was deleted from the database if err := service.DeleteChatPrompt(context.Background(), prompt1.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } if err := service.DeleteChatPrompt(context.Background(), prompt2.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } promptsAfterDelete, _ := service.GetAllChatPrompts(context.Background()) if len(promptsAfterDelete) != 0 { t.Error("retrieved chat prompts") } fmt.Printf("%+v", promptsAfterDelete) } func TestGetChatPromptsByTopic(t *testing.T) { // Create a new ChatPromptService with the test database connection q := sqlc_queries.New(db) service := NewChatPromptService(q) // Insert two chat prompts into the database with different topics prompt1_params := sqlc_queries.CreateChatPromptParams{ChatSessionUuid: "Test Topic 1", Role: "Test Role 1", Content: "Test Content 1", UserID: 1} prompt1, err := service.CreateChatPrompt(context.Background(), prompt1_params) if err != nil { t.Fatalf("failed to create chat prompt: %v", err) } prompt2_params := sqlc_queries.CreateChatPromptParams{ChatSessionUuid: "Test Topic 2", Role: "Test Role 2", Content: "Test Content 2", UserID: 2} prompt2, err := service.CreateChatPrompt(context.Background(), prompt2_params) if err != nil { t.Fatalf("failed to create chat prompt: %v", err) } // Retrieve chat prompts by topic and check that they match the expected values topic := "Test Topic 1" prompts, err := service.GetChatPromptsBySessionUUID(context.Background(), topic) if err != nil { t.Fatalf("failed to retrieve chat prompts: %v", err) } if len(prompts) != 1 { t.Errorf("expected 1 chat prompt, but got %d", len(prompts)) } if prompts[0].ChatSessionUuid != prompt1.ChatSessionUuid || prompts[0].Role != prompt1.Role || prompts[0].Content != prompt1.Content || prompts[0].Score != prompt1.Score || prompts[0].UserID != prompt1.UserID { t.Error("retrieved chat prompts do not match expected values") } // Delete the chat prompt and check that it was deleted from the database if err := service.DeleteChatPrompt(context.Background(), prompt1.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } if err := service.DeleteChatPrompt(context.Background(), prompt2.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } promptsAfterDelete, _ := service.GetAllChatPrompts(context.Background()) if len(promptsAfterDelete) != 0 { t.Error("retrieved chat prompts") } fmt.Printf("%+v", promptsAfterDelete) } func TestGetChatPromptsByUserID(t *testing.T) { // Create a new ChatPromptService with the test database connection q := sqlc_queries.New(db) service := NewChatPromptService(q) // Insert two chat prompts into the database with different user IDs prompt1_params := sqlc_queries.CreateChatPromptParams{ChatSessionUuid: "Test Topic 1", Role: "Test Role 1", Content: "Test Content 1", UserID: 1} prompt1, err := service.CreateChatPrompt(context.Background(), prompt1_params) if err != nil { t.Fatalf("failed to create chat prompt: %v", err) } prompt2_params := sqlc_queries.CreateChatPromptParams{ChatSessionUuid: "Test Topic 2", Role: "Test Role 2", Content: "Test Content 2", UserID: 2} prompt2, err := service.CreateChatPrompt(context.Background(), prompt2_params) if err != nil { t.Fatalf("failed to create chat prompt: %v", err) } // Retrieve chat prompts by user ID and check that they match the expected values userID := int32(1) prompts, err := service.GetChatPromptsByUserID(context.Background(), userID) if err != nil { t.Fatalf("failed to retrieve chat prompts: %v", err) } if len(prompts) != 1 { t.Errorf("expected 1 chat prompt, but got %d", len(prompts)) } if prompts[0].ChatSessionUuid != prompt1.ChatSessionUuid || prompts[0].Role != prompt1.Role || prompts[0].Content != prompt1.Content || prompts[0].Score != prompt1.Score || prompts[0].UserID != prompt1.UserID { t.Error("retrieved chat prompts do not match expected values") } // Delete the chat prompt and check that it was deleted from the database if err := service.DeleteChatPrompt(context.Background(), prompt1.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } if err := service.DeleteChatPrompt(context.Background(), prompt2.ID); err != nil { t.Fatalf("failed to delete chat prompt: %v", err) } promptsAfterDelete, _ := service.GetAllChatPrompts(context.Background()) if len(promptsAfterDelete) != 0 { t.Error("retrieved chat prompts") } fmt.Printf("%+v", promptsAfterDelete) } ================================================ FILE: api/chat_session_handler.go ================================================ package main import ( "database/sql" "encoding/json" "net/http" "strconv" "github.com/google/uuid" "github.com/gorilla/mux" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatSessionHandler struct { service *ChatSessionService } func NewChatSessionHandler(sqlc_q *sqlc_queries.Queries) *ChatSessionHandler { // create a new ChatSessionService instance chatSessionService := NewChatSessionService(sqlc_q) return &ChatSessionHandler{ service: chatSessionService, } } func (h *ChatSessionHandler) Register(router *mux.Router) { router.HandleFunc("/chat_sessions/user", h.getSimpleChatSessionsByUserID).Methods(http.MethodGet) router.HandleFunc("/uuid/chat_sessions/max_length/{uuid}", h.updateSessionMaxLength).Methods("PUT") router.HandleFunc("/uuid/chat_sessions/topic/{uuid}", h.updateChatSessionTopicByUUID).Methods("PUT") router.HandleFunc("/uuid/chat_sessions/{uuid}", h.getChatSessionByUUID).Methods("GET") router.HandleFunc("/uuid/chat_sessions/{uuid}", h.createOrUpdateChatSessionByUUID).Methods("PUT") router.HandleFunc("/uuid/chat_sessions/{uuid}", h.deleteChatSessionByUUID).Methods("DELETE") router.HandleFunc("/uuid/chat_sessions", h.createChatSessionByUUID).Methods("POST") router.HandleFunc("/uuid/chat_session_from_snapshot/{uuid}", h.createChatSessionFromSnapshot).Methods(http.MethodPost) } // getChatSessionByUUID returns a chat session by its UUID func (h *ChatSessionHandler) getChatSessionByUUID(w http.ResponseWriter, r *http.Request) { uuid := mux.Vars(r)["uuid"] session, err := h.service.GetChatSessionByUUID(r.Context(), uuid) if err != nil { if err == sql.ErrNoRows { apiErr := ErrResourceNotFound("Chat session") apiErr.Message = "Session not found with UUID: " + uuid RespondWithAPIError(w, apiErr) return } else { apiErr := WrapError(MapDatabaseError(err), "Failed to get chat session") RespondWithAPIError(w, apiErr) return } } session_resp := &ChatSessionResponse{ Uuid: session.Uuid, Topic: session.Topic, MaxLength: session.MaxLength, CreatedAt: session.CreatedAt, UpdatedAt: session.UpdatedAt, ArtifactEnabled: session.ArtifactEnabled, } json.NewEncoder(w).Encode(session_resp) } // createChatSessionByUUID creates a chat session by its UUID (idempotent) func (h *ChatSessionHandler) createChatSessionByUUID(w http.ResponseWriter, r *http.Request) { var req struct { Uuid string `json:"uuid"` Topic string `json:"topic"` Model string `json:"model"` DefaultSystemPrompt string `json:"defaultSystemPrompt"` } err := json.NewDecoder(r.Body).Decode(&req) if err != nil { apiErr := ErrValidationInvalidInput("Invalid request format") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } ctx := r.Context() userIDInt, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Get or create default workspace for the user workspaceService := NewChatWorkspaceService(h.service.q) defaultWorkspace, err := workspaceService.EnsureDefaultWorkspaceExists(ctx, userIDInt) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to ensure default workspace exists") RespondWithAPIError(w, apiErr) return } // Use CreateOrUpdateChatSessionByUUID for idempotent session creation createOrUpdateParams := sqlc_queries.CreateOrUpdateChatSessionByUUIDParams{ Uuid: req.Uuid, UserID: userIDInt, Topic: req.Topic, MaxLength: DefaultMaxLength, Temperature: DefaultTemperature, Model: req.Model, MaxTokens: DefaultMaxTokens, TopP: DefaultTopP, N: DefaultN, Debug: false, SummarizeMode: false, ExploreMode: false, ArtifactEnabled: false, WorkspaceID: sql.NullInt32{Int32: defaultWorkspace.ID, Valid: true}, } session, err := h.service.CreateOrUpdateChatSessionByUUID(r.Context(), createOrUpdateParams) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to create or update chat session") RespondWithAPIError(w, apiErr) return } _, err = h.service.EnsureDefaultSystemPrompt(ctx, session.Uuid, userIDInt, req.DefaultSystemPrompt) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to create default system prompt") RespondWithAPIError(w, apiErr) return } // set active chat session when creating a new chat session (use unified approach) _, err = h.service.q.UpsertUserActiveSession(r.Context(), sqlc_queries.UpsertUserActiveSessionParams{ UserID: session.UserID, WorkspaceID: sql.NullInt32{Valid: false}, ChatSessionUuid: session.Uuid, }) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to update or create active user session record") RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode(session) } type UpdateChatSessionRequest struct { Uuid string `json:"uuid"` Topic string `json:"topic"` MaxLength int32 `json:"maxLength"` Temperature float64 `json:"temperature"` Model string `json:"model"` TopP float64 `json:"topP"` N int32 `json:"n"` MaxTokens int32 `json:"maxTokens"` Debug bool `json:"debug"` SummarizeMode bool `json:"summarizeMode"` ArtifactEnabled bool `json:"artifactEnabled"` ExploreMode bool `json:"exploreMode"` WorkspaceUUID string `json:"workspaceUuid,omitempty"` } // UpdateChatSessionByUUID updates a chat session by its UUID func (h *ChatSessionHandler) createOrUpdateChatSessionByUUID(w http.ResponseWriter, r *http.Request) { var sessionReq UpdateChatSessionRequest err := json.NewDecoder(r.Body).Decode(&sessionReq) if err != nil { apiErr := ErrValidationInvalidInput("Invalid request format") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } if sessionReq.MaxLength == 0 { sessionReq.MaxLength = DefaultMaxLength } ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } var sessionParams sqlc_queries.CreateOrUpdateChatSessionByUUIDParams sessionParams.MaxLength = sessionReq.MaxLength sessionParams.Topic = sessionReq.Topic sessionParams.Uuid = sessionReq.Uuid sessionParams.UserID = userID sessionParams.Temperature = sessionReq.Temperature sessionParams.Model = sessionReq.Model sessionParams.TopP = sessionReq.TopP sessionParams.N = sessionReq.N sessionParams.MaxTokens = sessionReq.MaxTokens sessionParams.Debug = sessionReq.Debug sessionParams.SummarizeMode = sessionReq.SummarizeMode sessionParams.ArtifactEnabled = sessionReq.ArtifactEnabled sessionParams.ExploreMode = sessionReq.ExploreMode // Handle workspace if sessionReq.WorkspaceUUID != "" { workspaceService := NewChatWorkspaceService(h.service.q) workspace, err := workspaceService.GetWorkspaceByUUID(ctx, sessionReq.WorkspaceUUID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Invalid workspace UUID") RespondWithAPIError(w, apiErr) return } sessionParams.WorkspaceID = sql.NullInt32{Int32: workspace.ID, Valid: true} } else { // Ensure default workspace exists workspaceService := NewChatWorkspaceService(h.service.q) defaultWorkspace, err := workspaceService.EnsureDefaultWorkspaceExists(ctx, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to ensure default workspace exists") RespondWithAPIError(w, apiErr) return } sessionParams.WorkspaceID = sql.NullInt32{Int32: defaultWorkspace.ID, Valid: true} } session, err := h.service.CreateOrUpdateChatSessionByUUID(r.Context(), sessionParams) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to create or update chat session" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode(session) } // deleteChatSessionByUUID deletes a chat session by its UUID func (h *ChatSessionHandler) deleteChatSessionByUUID(w http.ResponseWriter, r *http.Request) { uuid := mux.Vars(r)["uuid"] err := h.service.DeleteChatSessionByUUID(r.Context(), uuid) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to delete chat session" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusOK) } // getSimpleChatSessionsByUserID returns a list of simple chat sessions by user ID func (h *ChatSessionHandler) getSimpleChatSessionsByUserID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() idStr := ctx.Value(userContextKey).(string) id, err := strconv.Atoi(idStr) if err != nil { apiErr := ErrValidationInvalidInput("Invalid user ID") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } sessions, err := h.service.GetSimpleChatSessionsByUserID(ctx, int32(id)) if err != nil { apiErr := ErrResourceNotFound("Chat sessions") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode(sessions) } // updateChatSessionTopicByUUID updates a chat session topic by its UUID func (h *ChatSessionHandler) updateChatSessionTopicByUUID(w http.ResponseWriter, r *http.Request) { uuid := mux.Vars(r)["uuid"] var sessionParams sqlc_queries.UpdateChatSessionTopicByUUIDParams err := json.NewDecoder(r.Body).Decode(&sessionParams) if err != nil { apiErr := ErrValidationInvalidInput("Invalid request format") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } sessionParams.Uuid = uuid ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } sessionParams.UserID = userID session, err := h.service.UpdateChatSessionTopicByUUID(r.Context(), sessionParams) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to update chat session topic" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode(session) } // updateSessionMaxLength func (h *ChatSessionHandler) updateSessionMaxLength(w http.ResponseWriter, r *http.Request) { uuid := mux.Vars(r)["uuid"] var sessionParams sqlc_queries.UpdateSessionMaxLengthParams err := json.NewDecoder(r.Body).Decode(&sessionParams) if err != nil { apiErr := ErrValidationInvalidInput("Invalid request format") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } sessionParams.Uuid = uuid session, err := h.service.UpdateSessionMaxLength(r.Context(), sessionParams) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to update session max length" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode(session) } // CreateChatSessionFromSnapshot ($uuid) // create a new session with title of snapshot, // create a prompt with the first message of snapshot // create messages based on the rest of messages. // return the new session uuid func (h *ChatSessionHandler) createChatSessionFromSnapshot(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) snapshot_uuid := vars["uuid"] userID, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } snapshot, err := h.service.q.ChatSnapshotByUUID(r.Context(), snapshot_uuid) if err != nil { apiErr := ErrResourceNotFound("Chat snapshot") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } sessionTitle := snapshot.Title conversions := snapshot.Conversation var conversionsSimpleMessages []SimpleChatMessage json.Unmarshal(conversions, &conversionsSimpleMessages) promptMsg := conversionsSimpleMessages[0] chatPrompt, err := h.service.q.GetChatPromptByUUID(r.Context(), promptMsg.Uuid) if err != nil { apiErr := ErrResourceNotFound("Chat prompt") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } originSession, err := h.service.q.GetChatSessionByUUIDWithInActive(r.Context(), chatPrompt.ChatSessionUuid) if err != nil { apiErr := ErrResourceNotFound("Original chat session") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } sessionUUID := uuid.New().String() session, err := h.service.q.CreateOrUpdateChatSessionByUUID(r.Context(), sqlc_queries.CreateOrUpdateChatSessionByUUIDParams{ Uuid: sessionUUID, UserID: userID, Topic: sessionTitle, MaxLength: originSession.MaxLength, Temperature: originSession.Temperature, Model: originSession.Model, MaxTokens: originSession.MaxTokens, TopP: originSession.TopP, Debug: originSession.Debug, SummarizeMode: originSession.SummarizeMode, ExploreMode: originSession.ExploreMode, WorkspaceID: originSession.WorkspaceID, N: 1, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to create chat session from snapshot" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } _, err = h.service.q.CreateChatPrompt(r.Context(), sqlc_queries.CreateChatPromptParams{ Uuid: NewUUID(), ChatSessionUuid: sessionUUID, Role: "system", Content: promptMsg.Text, UserID: userID, CreatedBy: userID, UpdatedBy: userID, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to create prompt for chat session from snapshot" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } for _, message := range conversionsSimpleMessages[1:] { // if inversion is true, the role is user, otherwise assistant // Determine the role based on the inversion flag messageParam := sqlc_queries.CreateChatMessageParams{ ChatSessionUuid: sessionUUID, Uuid: NewUUID(), Role: message.GetRole(), Content: message.Text, UserID: userID, Raw: json.RawMessage([]byte("{}")), } _, err = h.service.q.CreateChatMessage(r.Context(), messageParam) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to create messages for chat session from snapshot" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } } // set active session using simplified service activeSessionService := NewUserActiveChatSessionService(h.service.q) _, err = activeSessionService.UpsertActiveSession(r.Context(), userID, nil, session.Uuid) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to update active session" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"SessionUuid": session.Uuid}) } ================================================ FILE: api/chat_session_service.go ================================================ package main import ( "context" "database/sql" "errors" "strings" "github.com/rotisserie/eris" "github.com/samber/lo" "github.com/swuecho/chat_backend/sqlc_queries" ) // ChatSessionService provides methods for interacting with chat sessions. type ChatSessionService struct { q *sqlc_queries.Queries } // NewChatSessionService creates a new ChatSessionService. func NewChatSessionService(q *sqlc_queries.Queries) *ChatSessionService { return &ChatSessionService{q: q} } // CreateChatSession creates a new chat session. func (s *ChatSessionService) CreateChatSession(ctx context.Context, session_params sqlc_queries.CreateChatSessionParams) (sqlc_queries.ChatSession, error) { session, err := s.q.CreateChatSession(ctx, session_params) if err != nil { return sqlc_queries.ChatSession{}, err } return session, nil } // GetChatSessionByID returns a chat session by ID. func (s *ChatSessionService) GetChatSessionByID(ctx context.Context, id int32) (sqlc_queries.ChatSession, error) { session, err := s.q.GetChatSessionByID(ctx, id) if err != nil { return sqlc_queries.ChatSession{}, eris.Wrap(err, "failed to retrieve session: ") } return session, nil } // UpdateChatSession updates an existing chat session. func (s *ChatSessionService) UpdateChatSession(ctx context.Context, session_params sqlc_queries.UpdateChatSessionParams) (sqlc_queries.ChatSession, error) { session_u, err := s.q.UpdateChatSession(ctx, session_params) if err != nil { return sqlc_queries.ChatSession{}, eris.Wrap(err, "failed to update session") } return session_u, nil } // DeleteChatSession deletes a chat session by ID. func (s *ChatSessionService) DeleteChatSession(ctx context.Context, id int32) error { err := s.q.DeleteChatSession(ctx, id) if err != nil { return eris.Wrap(err, "failed to delete session by id") } return nil } // GetAllChatSessions returns all chat sessions. func (s *ChatSessionService) GetAllChatSessions(ctx context.Context) ([]sqlc_queries.ChatSession, error) { sessions, err := s.q.GetAllChatSessions(ctx) if err != nil { return nil, eris.Wrap(err, "failed to retrieve sessions") } return sessions, nil } func (s *ChatSessionService) GetChatSessionsByUserID(ctx context.Context, userID int32) ([]sqlc_queries.ChatSession, error) { sessions, err := s.q.GetChatSessionsByUserID(ctx, userID) if err != nil { return nil, eris.Wrap(err, "failed to retrieve sessions") } return sessions, nil } func (s *ChatSessionService) GetSimpleChatSessionsByUserID(ctx context.Context, userID int32) ([]SimpleChatSession, error) { sessions, err := s.q.GetSessionsGroupedByWorkspace(ctx, userID) if err != nil { return nil, err } simple_sessions := lo.Map(sessions, func(session sqlc_queries.GetSessionsGroupedByWorkspaceRow, _idx int) SimpleChatSession { workspaceUuid := "" if session.WorkspaceUuid.Valid { workspaceUuid = session.WorkspaceUuid.String } return SimpleChatSession{ Uuid: session.Uuid, IsEdit: false, Title: session.Topic, MaxLength: int(session.MaxLength), Temperature: float64(session.Temperature), TopP: float64(session.TopP), N: session.N, MaxTokens: session.MaxTokens, Debug: session.Debug, Model: session.Model, SummarizeMode: session.SummarizeMode, ArtifactEnabled: session.ArtifactEnabled, WorkspaceUuid: workspaceUuid, } }) return simple_sessions, nil } // GetChatSessionByUUID returns an authentication user record by ID. func (s *ChatSessionService) GetChatSessionByUUID(ctx context.Context, uuid string) (sqlc_queries.ChatSession, error) { chatSession, err := s.q.GetChatSessionByUUID(ctx, uuid) if err != nil { return sqlc_queries.ChatSession{}, eris.Wrap(err, "failed to retrieve session by uuid, ") } return chatSession, nil } // UpdateChatSessionByUUID updates an existing chat session. func (s *ChatSessionService) UpdateChatSessionByUUID(ctx context.Context, session_params sqlc_queries.UpdateChatSessionByUUIDParams) (sqlc_queries.ChatSession, error) { session_u, err := s.q.UpdateChatSessionByUUID(ctx, session_params) if err != nil { return sqlc_queries.ChatSession{}, eris.Wrap(err, "failed to update session, ") } return session_u, nil } // UpdateChatSessionTopicByUUID updates an existing chat session topic. func (s *ChatSessionService) UpdateChatSessionTopicByUUID(ctx context.Context, session_params sqlc_queries.UpdateChatSessionTopicByUUIDParams) (sqlc_queries.ChatSession, error) { session_u, err := s.q.UpdateChatSessionTopicByUUID(ctx, session_params) if err != nil { return sqlc_queries.ChatSession{}, eris.Wrap(err, "failed to update session, ") } return session_u, nil } // CreateOrUpdateChatSessionByUUID updates an existing chat session. func (s *ChatSessionService) CreateOrUpdateChatSessionByUUID(ctx context.Context, session_params sqlc_queries.CreateOrUpdateChatSessionByUUIDParams) (sqlc_queries.ChatSession, error) { session_u, err := s.q.CreateOrUpdateChatSessionByUUID(ctx, session_params) if err != nil { return sqlc_queries.ChatSession{}, eris.Wrap(err, "failed to update session, ") } return session_u, nil } // DeleteChatSessionByUUID deletes a chat session by UUID. func (s *ChatSessionService) DeleteChatSessionByUUID(ctx context.Context, uuid string) error { err := s.q.DeleteChatSessionByUUID(ctx, uuid) if err != nil { return eris.Wrap(err, "failed to delete session by uuid, ") } return nil } // UpdateSessionMaxLength func (s *ChatSessionService) UpdateSessionMaxLength(ctx context.Context, session_params sqlc_queries.UpdateSessionMaxLengthParams) (sqlc_queries.ChatSession, error) { session_u, err := s.q.UpdateSessionMaxLength(ctx, session_params) if err != nil { return sqlc_queries.ChatSession{}, eris.Wrap(err, "failed to update session, ") } return session_u, nil } // EnsureDefaultSystemPrompt ensures a session has exactly one active system prompt. // It is safe to call repeatedly and tolerates concurrent callers. func (s *ChatSessionService) EnsureDefaultSystemPrompt(ctx context.Context, chatSessionUUID string, userID int32, systemPrompt string) (sqlc_queries.ChatPrompt, error) { existingPrompt, err := s.q.GetOneChatPromptBySessionUUID(ctx, chatSessionUUID) if err == nil { return existingPrompt, nil } if !errors.Is(err, sql.ErrNoRows) { return sqlc_queries.ChatPrompt{}, eris.Wrap(err, "failed to check existing session prompt") } promptText := strings.TrimSpace(systemPrompt) if promptText == "" { promptText = DefaultSystemPromptText } tokenCount, tokenErr := getTokenCount(promptText) if tokenErr != nil { tokenCount = len(promptText) / TokenEstimateRatio } if tokenCount <= 0 { tokenCount = 1 } prompt, createErr := s.q.CreateChatPrompt(ctx, sqlc_queries.CreateChatPromptParams{ Uuid: NewUUID(), ChatSessionUuid: chatSessionUUID, Role: "system", Content: promptText, TokenCount: int32(tokenCount), UserID: userID, CreatedBy: userID, UpdatedBy: userID, }) if createErr == nil { return prompt, nil } // Handle concurrent creation race by returning the now-existing prompt. existingPrompt, err = s.q.GetOneChatPromptBySessionUUID(ctx, chatSessionUUID) if err == nil { return existingPrompt, nil } return sqlc_queries.ChatPrompt{}, eris.Wrap(createErr, "failed to create default system prompt") } ================================================ FILE: api/chat_session_service_test.go ================================================ package main import ( "context" "database/sql" "errors" "fmt" "testing" _ "github.com/lib/pq" "github.com/swuecho/chat_backend/sqlc_queries" ) func TestChatSessionService(t *testing.T) { sqlc_q := sqlc_queries.New(db) service := NewChatSessionService(sqlc_q) // Create a new database connection // Insert a new chat session into the database session_params := sqlc_queries.CreateChatSessionParams{UserID: 1, Topic: "Test Session", MaxLength: 100} session, err := service.CreateChatSession(context.Background(), session_params) if err != nil { t.Fatalf("failed to create chat session: %v", err) } // Retrieve the chat session by ID and check that it matches the expected values retrievedSession, err := service.GetChatSessionByID(context.Background(), session.ID) if err != nil { t.Fatalf("failed to get chat session: %v", err) } if retrievedSession.UserID != session.UserID || retrievedSession.Topic != session.Topic || retrievedSession.MaxLength != session.MaxLength { t.Error("retrieved chat session does not match expected values") } // Update the chat session and check that it was updated in the database updated_params := sqlc_queries.UpdateChatSessionParams{ID: session.ID, UserID: session.UserID, Topic: "Updated Test Session", } if _, err := service.UpdateChatSession(context.Background(), updated_params); err != nil { t.Fatalf("failed to update chat session: %v", err) } retrievedSession, err = service.GetChatSessionByID(context.Background(), session.ID) if err != nil { t.Fatalf("failed to get chat session: %v", err) } // Check that updated chat session matches expected values // UpdatedAt is generated by the database; verify updated fields instead of strict timestamp equality. if retrievedSession.Topic != updated_params.Topic { t.Errorf("chat session mismatch: expected Topic=%s, got Topic=%s ", updated_params.Topic, retrievedSession.Topic) } // Delete the chat session and check that it was deleted from the database if err := service.DeleteChatSession(context.Background(), session.ID); err != nil { t.Fatalf("failed to delete chat session: %v", err) } deletedSession, err := service.GetChatSessionByID(context.Background(), session.ID) if err == nil || !errors.Is(err, sql.ErrNoRows) { fmt.Printf("%+v", deletedSession) t.Error("expected error due to missing chat session, but got no error or different error") } } func TestGetChatSessionsByUserID(t *testing.T) { sqlc_q := sqlc_queries.New(db) service := NewChatSessionService(sqlc_q) // Insert two chat sessions into the database with different user IDs session1_params := sqlc_queries.CreateChatSessionParams{UserID: 1, Topic: "Test Session 1", MaxLength: 100, Uuid: "uuid1"} session1, err := service.CreateChatSession(context.Background(), session1_params) if err != nil { t.Fatalf("failed to create chat session: %v", err) } session2_params := sqlc_queries.CreateChatSessionParams{UserID: 2, Topic: "Test Session 2", MaxLength: 150, Uuid: "uuid2"} session2, err := service.CreateChatSession(context.Background(), session2_params) if err != nil { t.Fatalf("failed to create chat session: %v", err) } // Retrieve chat sessions by user ID and check that they match the expected values userID := int32(1) sessions, err := service.GetChatSessionsByUserID(context.Background(), userID) if err != nil { t.Fatalf("failed to retrieve chat sessions: %v", err) } if len(sessions) != 1 { t.Errorf("expected 1 chat session, but got %d", len(sessions)) } if sessions[0].UserID != session1.UserID || sessions[0].Topic != session1.Topic || sessions[0].MaxLength != session1.MaxLength { t.Error("retrieved chat sessions do not match expected values") } if err := service.DeleteChatSession(context.Background(), session1.ID); err != nil { t.Fatalf("failed to delete chat session: %v", err) } if err := service.DeleteChatSession(context.Background(), session2.ID); err != nil { t.Fatalf("failed to delete chat session: %v", err) } } func TestGetAllChatSessions(t *testing.T) { // Create a new ChatSessionService with the test database connection q := sqlc_queries.New(db) service := NewChatSessionService(q) session1_params := sqlc_queries.CreateChatSessionParams{UserID: 1, Topic: "Test Session 1", MaxLength: 100, Uuid: "uuid1"} session1, err := service.CreateChatSession(context.Background(), session1_params) if err != nil { t.Fatalf("failed to create chat session: %v", err) } session2_params := sqlc_queries.CreateChatSessionParams{UserID: 2, Topic: "Test Session 2", MaxLength: 150, Uuid: "uuid2"} session2, err := service.CreateChatSession(context.Background(), session2_params) if err != nil { t.Fatalf("failed to create chat session: %v", err) } // Retrieve all chat sessions and check that they match the expected values sessions, err := service.GetAllChatSessions(context.Background()) if err != nil { t.Fatalf("failed to retrieve chat sessions: %v", err) } if len(sessions) != 2 { t.Errorf("expected 2 chat sessions, but got %d", len(sessions)) } if sessions[0].Topic != session1.Topic || sessions[1].Topic != session2.Topic { t.Error("retrieved chat sessions do not match expected values") } if err := service.DeleteChatSession(context.Background(), session1.ID); err != nil { t.Fatalf("failed to delete chat session: %v", err) } if err := service.DeleteChatSession(context.Background(), session2.ID); err != nil { t.Fatalf("failed to delete chat session: %v", err) } } ================================================ FILE: api/chat_snapshot_handler.go ================================================ package main import ( "encoding/json" "net/http" "strconv" "github.com/gorilla/mux" "github.com/swuecho/chat_backend/sqlc_queries" ) // ChatSnapshotHandler handles requests related to chat snapshots type ChatSnapshotHandler struct { service *ChatSnapshotService } // NewChatSnapshotHandler creates a new handler instance func NewChatSnapshotHandler(sqlc_q *sqlc_queries.Queries) *ChatSnapshotHandler { return &ChatSnapshotHandler{ service: NewChatSnapshotService(sqlc_q), } } func (h *ChatSnapshotHandler) Register(router *mux.Router) { router.HandleFunc("/uuid/chat_snapshot/all", h.ChatSnapshotMetaByUserID).Methods(http.MethodGet) router.HandleFunc("/uuid/chat_snapshot/{uuid}", h.GetChatSnapshot).Methods(http.MethodGet) router.HandleFunc("/uuid/chat_snapshot/{uuid}", h.CreateChatSnapshot).Methods(http.MethodPost) router.HandleFunc("/uuid/chat_snapshot/{uuid}", h.UpdateChatSnapshotMetaByUUID).Methods(http.MethodPut) router.HandleFunc("/uuid/chat_snapshot/{uuid}", h.DeleteChatSnapshot).Methods(http.MethodDelete) router.HandleFunc("/uuid/chat_snapshot_search", h.ChatSnapshotSearch).Methods(http.MethodGet) router.HandleFunc("/uuid/chat_bot/{uuid}", h.CreateChatBot).Methods(http.MethodPost) } func (h *ChatSnapshotHandler) CreateChatSnapshot(w http.ResponseWriter, r *http.Request) { chatSessionUuid := mux.Vars(r)["uuid"] user_id, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } uuid, err := h.service.CreateChatSnapshot(r.Context(), chatSessionUuid, user_id) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to create chat snapshot") RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode( map[string]interface{}{ "uuid": uuid, }) } func (h *ChatSnapshotHandler) CreateChatBot(w http.ResponseWriter, r *http.Request) { chatSessionUuid := mux.Vars(r)["uuid"] user_id, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } uuid, err := h.service.CreateChatBot(r.Context(), chatSessionUuid, user_id) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to create chat bot" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode( map[string]interface{}{ "uuid": uuid, }) } func (h *ChatSnapshotHandler) GetChatSnapshot(w http.ResponseWriter, r *http.Request) { uuidStr := mux.Vars(r)["uuid"] snapshot, err := h.service.q.ChatSnapshotByUUID(r.Context(), uuidStr) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to get chat snapshot") RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode(snapshot) } func (h *ChatSnapshotHandler) ChatSnapshotMetaByUserID(w http.ResponseWriter, r *http.Request) { userID, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // get type from query typ := r.URL.Query().Get("type") // Parse pagination parameters pageStr := r.URL.Query().Get("page") pageSizeStr := r.URL.Query().Get("page_size") page := int32(1) // Default to page 1 pageSize := int32(20) // Default to 20 items per page if pageStr != "" { parsedPage, err := strconv.Atoi(pageStr) if err == nil && parsedPage > 0 { page = int32(parsedPage) } } if pageSizeStr != "" { parsedPageSize, err := strconv.Atoi(pageSizeStr) if err == nil && parsedPageSize > 0 && parsedPageSize <= 100 { pageSize = int32(parsedPageSize) } } offset := (page - 1) * pageSize chatSnapshots, err := h.service.q.ChatSnapshotMetaByUserID(r.Context(), sqlc_queries.ChatSnapshotMetaByUserIDParams{ UserID: userID, Typ: typ, Limit: pageSize, Offset: offset, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to retrieve chat snapshots" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Get total count for pagination totalCount, err := h.service.q.ChatSnapshotCountByUserIDAndType(r.Context(), sqlc_queries.ChatSnapshotCountByUserIDAndTypeParams{ UserID: userID, Column2: typ, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to retrieve snapshot count" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "data": chatSnapshots, "page": page, "page_size": pageSize, "total": totalCount, }) } func (h *ChatSnapshotHandler) UpdateChatSnapshotMetaByUUID(w http.ResponseWriter, r *http.Request) { uuid := mux.Vars(r)["uuid"] var input struct { Title string `json:"title"` Summary string `json:"summary"` } err := json.NewDecoder(r.Body).Decode(&input) if err != nil { apiErr := ErrValidationInvalidInput("Failed to parse request body") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } userID, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } err = h.service.q.UpdateChatSnapshotMetaByUUID(r.Context(), sqlc_queries.UpdateChatSnapshotMetaByUUIDParams{ Uuid: uuid, Title: input.Title, Summary: input.Summary, UserID: userID, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to update chat snapshot metadata" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } snapshot, err := h.service.q.ChatSnapshotByUUID(r.Context(), uuid) if err != nil { apiErr := ErrResourceNotFound("Chat snapshot") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode(snapshot) } func (h *ChatSnapshotHandler) DeleteChatSnapshot(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) uuid := vars["uuid"] userID, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } _, err = h.service.q.DeleteChatSnapshot(r.Context(), sqlc_queries.DeleteChatSnapshotParams{ Uuid: uuid, UserID: userID, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to delete chat snapshot" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } } func (h *ChatSnapshotHandler) ChatSnapshotSearch(w http.ResponseWriter, r *http.Request) { search := r.URL.Query().Get("search") if search == "" { w.WriteHeader(http.StatusOK) var emptySlice []any // create an empty slice of integers json.NewEncoder(w).Encode(emptySlice) return } userID, err := getUserID(r.Context()) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } chatSnapshots, err := h.service.q.ChatSnapshotSearch(r.Context(), sqlc_queries.ChatSnapshotSearchParams{ UserID: userID, Search: search, }) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Failed to search chat snapshots" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(chatSnapshots) } ================================================ FILE: api/chat_snapshot_handler_test.go ================================================ package main import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gorilla/mux" "github.com/swuecho/chat_backend/sqlc_queries" "gotest.tools/v3/assert" ) // the code below do db update directly in instead of using handler, please change to use handler // TestChatSnapshot tests the ChatSnapshotHandler func TestChatSnapshot(t *testing.T) { const snapshotPath = "/uuid/chat_snapshot/%s" // API path for snapshots // Create a chat service for testing q := sqlc_queries.New(db) h := NewChatSnapshotHandler(q) // Create a ChatSnapshotHandler // Register snapshot API routes router := mux.NewRouter() h.Register(router) // Add a test user userID := 1 // Generate a random UUID for the snapshot snapshotUUID := NewUUID() // Create a test snapshot snapshot, err := h.service.q.CreateChatSnapshot(context.Background(), sqlc_queries.CreateChatSnapshotParams{ Uuid: snapshotUUID, // Use the generated UUID Model: "gpt3", Title: "test chat snapshot", UserID: int32(userID), Session: json.RawMessage([]byte("{}")), Tags: json.RawMessage([]byte("{}")), Text: "test chat snapshot text", Conversation: json.RawMessage([]byte("{}")), }) if err != nil { return } assert.Equal(t, snapshot.Uuid, snapshotUUID) // Test GET snapshot - should succeed req, _ := http.NewRequest("GET", fmt.Sprintf(snapshotPath, snapshot.Uuid), nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) // Test DELETE snapshot without auth - should fail reqDelete, _ := http.NewRequest("DELETE", fmt.Sprintf(snapshotPath, snapshot.Uuid), nil) rr = httptest.NewRecorder() router.ServeHTTP(rr, reqDelete) assert.Equal(t, http.StatusUnauthorized, rr.Code) // Test DELETE snapshot with auth - should succeed reqDeleteWithAuth, _ := http.NewRequest("DELETE", fmt.Sprintf(snapshotPath, snapshot.Uuid), nil) ctx := getContextWithUser(userID) // Get auth context reqDeleteWithAuth = reqDeleteWithAuth.WithContext(ctx) rr = httptest.NewRecorder() router.ServeHTTP(rr, reqDeleteWithAuth) assert.Equal(t, http.StatusOK, rr.Code) } ================================================ FILE: api/chat_snapshot_service.go ================================================ package main import ( "context" "encoding/json" "log" "github.com/google/uuid" "github.com/samber/lo" "github.com/swuecho/chat_backend/sqlc_queries" ) // ChatSnapshotService provides methods for interacting with chat sessions. type ChatSnapshotService struct { q *sqlc_queries.Queries } // NewChatSnapshotService creates a new ChatSnapshotService. func NewChatSnapshotService(q *sqlc_queries.Queries) *ChatSnapshotService { return &ChatSnapshotService{q: q} } func (s *ChatSnapshotService) CreateChatSnapshot(ctx context.Context, chatSessionUuid string, userId int32) (string, error) { chatSession, err := s.q.GetChatSessionByUUID(ctx, chatSessionUuid) if err != nil { return "", err } // TODO: fix hardcode // Get chat history simple_msgs, err := s.q.GetChatHistoryBySessionUUID(ctx, chatSessionUuid, int32(1), int32(10000)) if err != nil { return "", err } text := lo.Reduce(simple_msgs, func(acc string, curr sqlc_queries.SimpleChatMessage, _ int) string { return acc + curr.Text }, "") title := GenTitle(s.q, ctx, chatSession, text) // simple_msgs to RawMessage simple_msgs_raw, err := json.Marshal(simple_msgs) if err != nil { return "", err } snapshot_uuid := uuid.New().String() chatSessionMessage, err := json.Marshal(chatSession) if err != nil { return "", err } one, err := s.q.CreateChatSnapshot(ctx, sqlc_queries.CreateChatSnapshotParams{ Uuid: snapshot_uuid, Model: chatSession.Model, Title: title, UserID: userId, Session: chatSessionMessage, Tags: json.RawMessage([]byte("{}")), Text: text, Conversation: simple_msgs_raw, }) if err != nil { log.Println(err) return "", err } return one.Uuid, nil } func GenTitle(q *sqlc_queries.Queries, ctx context.Context, chatSession sqlc_queries.ChatSession, text string) string { title := firstN(chatSession.Topic, 100) // generate title using model := "gemini-2.0-flash" _, err := q.ChatModelByName(ctx, model) if err == nil { genTitle, err := GenerateChatTitle(ctx, model, text) if err != nil { log.Println(err) } if genTitle != "" { title = genTitle } } return title } func (s *ChatSnapshotService) CreateChatBot(ctx context.Context, chatSessionUuid string, userId int32) (string, error) { chatSession, err := s.q.GetChatSessionByUUID(ctx, chatSessionUuid) if err != nil { return "", err } // TODO: fix hardcode simple_msgs, err := s.q.GetChatHistoryBySessionUUID(ctx, chatSessionUuid, int32(1), int32(10000)) text := lo.Reduce(simple_msgs, func(acc string, curr sqlc_queries.SimpleChatMessage, _ int) string { return acc + curr.Text }, "") // save all simple_msgs to a jsonb field in chat_snapshot if err != nil { return "", err } // simple_msgs to RawMessage simple_msgs_raw, err := json.Marshal(simple_msgs) if err != nil { return "", err } snapshot_uuid := uuid.New().String() chatSessionMessage, err := json.Marshal(chatSession) if err != nil { return "", err } title := GenTitle(s.q, ctx, chatSession, text) one, err := s.q.CreateChatBot(ctx, sqlc_queries.CreateChatBotParams{ Uuid: snapshot_uuid, Model: chatSession.Model, Typ: "chatbot", Title: title, UserID: userId, Session: chatSessionMessage, Tags: json.RawMessage([]byte("{}")), Text: text, Conversation: simple_msgs_raw, }) if err != nil { log.Println(err) return "", err } return one.Uuid, nil } ================================================ FILE: api/chat_user_active_chat_session_handler.go ================================================ package main import ( "database/sql" "encoding/json" "errors" "fmt" "log" "net/http" "regexp" "github.com/gorilla/mux" sqlc "github.com/swuecho/chat_backend/sqlc_queries" ) // UserActiveChatSessionHandler handles requests related to active chat sessions type UserActiveChatSessionHandler struct { service *UserActiveChatSessionService } // NewUserActiveChatSessionHandler creates a new handler instance func NewUserActiveChatSessionHandler(sqlc_q *sqlc.Queries) *UserActiveChatSessionHandler { activeSessionService := NewUserActiveChatSessionService(sqlc_q) return &UserActiveChatSessionHandler{ service: activeSessionService, } } // Register sets up the handler routes func (h *UserActiveChatSessionHandler) Register(router *mux.Router) { router.HandleFunc("/uuid/user_active_chat_session", h.GetUserActiveChatSessionHandler).Methods(http.MethodGet) router.HandleFunc("/uuid/user_active_chat_session", h.CreateOrUpdateUserActiveChatSessionHandler).Methods(http.MethodPut) // Per-workspace active session endpoints // Note: More specific routes must come before parameterized routes to avoid shadowing router.HandleFunc("/workspaces/active-sessions", h.GetAllWorkspaceActiveSessionsHandler).Methods(http.MethodGet) router.HandleFunc("/workspaces/{workspaceUuid}/active-session", h.GetWorkspaceActiveSessionHandler).Methods(http.MethodGet) router.HandleFunc("/workspaces/{workspaceUuid}/active-session", h.SetWorkspaceActiveSessionHandler).Methods(http.MethodPut) } // GetUserActiveChatSessionHandler handles GET requests to get a session by user_id func (h *UserActiveChatSessionHandler) GetUserActiveChatSessionHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get and validate user ID userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } log.Printf("Getting active chat session for user %d", userID) // Get session from service (use unified approach for global session) session, err := h.service.GetActiveSession(r.Context(), userID, nil) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithAPIError(w, ErrChatSessionNotFound.WithMessage(fmt.Sprintf("no active session for user %d", userID))) } else { RespondWithAPIError(w, WrapError(err, "failed to get active chat session")) } return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(session); err != nil { log.Printf("Failed to encode response: %v", err) } } // UUID validation regex var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) // CreateOrUpdateUserActiveChatSessionHandler handles PUT requests to create/update a session func (h *UserActiveChatSessionHandler) CreateOrUpdateUserActiveChatSessionHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get and validate user ID userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } // Parse request body var reqBody struct { ChatSessionUuid string `json:"chatSessionUuid"` } if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("failed to parse request body")) return } // Validate session UUID format if !uuidRegex.MatchString(reqBody.ChatSessionUuid) { RespondWithAPIError(w, ErrChatSessionInvalid.WithMessage( fmt.Sprintf("invalid session UUID format: %s", reqBody.ChatSessionUuid))) return } log.Printf("Creating/updating active chat session for user %d", userID) // Create/update session (use unified approach for global session) session, err := h.service.UpsertActiveSession(r.Context(), userID, nil, reqBody.ChatSessionUuid) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to create or update active chat session")) return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(session); err != nil { log.Printf("Failed to encode response: %v", err) } } // Per-workspace active session handlers // GetWorkspaceActiveSessionHandler gets the active session for a specific workspace func (h *UserActiveChatSessionHandler) GetWorkspaceActiveSessionHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() workspaceUuid := mux.Vars(r)["workspaceUuid"] // Get and validate user ID userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } // Check workspace permission workspaceService := NewChatWorkspaceService(h.service.q) hasPermission, err := workspaceService.HasWorkspacePermission(ctx, workspaceUuid, userID) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to check workspace permission")) return } if !hasPermission { RespondWithAPIError(w, ErrAuthAccessDenied.WithMessage("access denied to workspace")) return } // Get workspace to get its ID workspace, err := workspaceService.GetWorkspaceByUUID(ctx, workspaceUuid) if err != nil { RespondWithAPIError(w, ErrResourceNotFound("Workspace").WithMessage("workspace not found")) return } // Get workspace active session session, err := h.service.GetActiveSession(ctx, userID, &workspace.ID) if err != nil { RespondWithAPIError(w, ErrResourceNotFound("Active Session").WithMessage("no active session for workspace")) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "chatSessionUuid": session.ChatSessionUuid, "workspaceUuid": workspaceUuid, "updatedAt": session.UpdatedAt, }) } // SetWorkspaceActiveSessionHandler sets the active session for a specific workspace func (h *UserActiveChatSessionHandler) SetWorkspaceActiveSessionHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() workspaceUuid := mux.Vars(r)["workspaceUuid"] // Get and validate user ID userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } // Check workspace permission workspaceService := NewChatWorkspaceService(h.service.q) hasPermission, err := workspaceService.HasWorkspacePermission(ctx, workspaceUuid, userID) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to check workspace permission")) return } if !hasPermission { RespondWithAPIError(w, ErrAuthAccessDenied.WithMessage("access denied to workspace")) return } // Parse request body var requestBody struct { ChatSessionUuid string `json:"chatSessionUuid"` } if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("failed to parse request body")) return } // Validate session UUID format if !uuidRegex.MatchString(requestBody.ChatSessionUuid) { RespondWithAPIError(w, ErrChatSessionInvalid.WithMessage("invalid session UUID format")) return } // Get workspace to get its ID workspace, err := workspaceService.GetWorkspaceByUUID(ctx, workspaceUuid) if err != nil { RespondWithAPIError(w, ErrResourceNotFound("Workspace").WithMessage("workspace not found")) return } sessionService := NewChatSessionService(h.service.q) session, err := sessionService.GetChatSessionByUUID(ctx, requestBody.ChatSessionUuid) if err != nil { RespondWithAPIError(w, ErrResourceNotFound("Chat Session").WithMessage("chat session not found")) return } if !session.WorkspaceID.Valid || session.WorkspaceID.Int32 != workspace.ID { RespondWithAPIError(w, ErrValidationInvalidInput("session does not belong to workspace")) return } // Set workspace active session activeSession, err := h.service.UpsertActiveSession(ctx, userID, &workspace.ID, requestBody.ChatSessionUuid) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to set workspace active session")) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "chatSessionUuid": activeSession.ChatSessionUuid, "workspaceUuid": workspaceUuid, "updatedAt": activeSession.UpdatedAt, }) } // GetAllWorkspaceActiveSessionsHandler gets all workspace active sessions for a user func (h *UserActiveChatSessionHandler) GetAllWorkspaceActiveSessionsHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get and validate user ID userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } // Get all workspace active sessions sessions, err := h.service.GetAllActiveSessions(ctx, userID) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to get workspace active sessions")) return } // Convert to response format with workspace UUIDs workspaceService := NewChatWorkspaceService(h.service.q) workspaces, err := workspaceService.GetWorkspacesByUserID(ctx, userID) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to get workspaces")) return } // Create a map for workspace ID to UUID lookup workspaceMap := make(map[int32]string) for _, workspace := range workspaces { workspaceMap[workspace.ID] = workspace.Uuid } // Build response var response []map[string]interface{} for _, session := range sessions { if session.WorkspaceID.Valid { if workspaceUuid, exists := workspaceMap[session.WorkspaceID.Int32]; exists { response = append(response, map[string]interface{}{ "workspaceUuid": workspaceUuid, "chatSessionUuid": session.ChatSessionUuid, "updatedAt": session.UpdatedAt, }) } } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } ================================================ FILE: api/chat_user_active_chat_session_sevice.go ================================================ package main import ( "context" "database/sql" "github.com/rotisserie/eris" sqlc "github.com/swuecho/chat_backend/sqlc_queries" ) type UserActiveChatSessionService struct { q *sqlc.Queries } func NewUserActiveChatSessionService(q *sqlc.Queries) *UserActiveChatSessionService { return &UserActiveChatSessionService{q: q} } // Simplified unified methods // UpsertActiveSession creates or updates an active session for a user in a specific workspace (or global if workspaceID is nil) func (s *UserActiveChatSessionService) UpsertActiveSession(ctx context.Context, userID int32, workspaceID *int32, sessionUUID string) (sqlc.UserActiveChatSession, error) { var nullWorkspaceID sql.NullInt32 if workspaceID != nil { nullWorkspaceID = sql.NullInt32{Int32: *workspaceID, Valid: true} } session, err := s.q.UpsertUserActiveSession(ctx, sqlc.UpsertUserActiveSessionParams{ UserID: userID, WorkspaceID: nullWorkspaceID, ChatSessionUuid: sessionUUID, }) if err != nil { return sqlc.UserActiveChatSession{}, eris.Wrap(err, "failed to upsert active session") } return session, nil } // GetActiveSession retrieves the active session for a user in a specific workspace (or global if workspaceID is nil) func (s *UserActiveChatSessionService) GetActiveSession(ctx context.Context, userID int32, workspaceID *int32) (sqlc.UserActiveChatSession, error) { var workspaceParam int32 if workspaceID != nil { workspaceParam = *workspaceID } session, err := s.q.GetUserActiveSession(ctx, sqlc.GetUserActiveSessionParams{ UserID: userID, Column2: workspaceParam, // SQLC generated this awkward name due to the complex WHERE clause }) if err != nil { return sqlc.UserActiveChatSession{}, eris.Wrap(err, "failed to get active session") } return session, nil } // GetAllActiveSessions retrieves all active sessions for a user (both global and workspace-specific) func (s *UserActiveChatSessionService) GetAllActiveSessions(ctx context.Context, userID int32) ([]sqlc.UserActiveChatSession, error) { sessions, err := s.q.GetAllUserActiveSessions(ctx, userID) if err != nil { return nil, eris.Wrap(err, "failed to get all active sessions") } return sessions, nil } // DeleteActiveSession deletes the active session for a user in a specific workspace (or global if workspaceID is nil) func (s *UserActiveChatSessionService) DeleteActiveSession(ctx context.Context, userID int32, workspaceID *int32) error { var workspaceParam int32 if workspaceID != nil { workspaceParam = *workspaceID } err := s.q.DeleteUserActiveSession(ctx, sqlc.DeleteUserActiveSessionParams{ UserID: userID, Column2: workspaceParam, // SQLC generated this awkward name }) if err != nil { return eris.Wrap(err, "failed to delete active session") } return nil } ================================================ FILE: api/chat_workspace_handler.go ================================================ package main import ( "context" "database/sql" "encoding/json" "log" "net/http" "time" "github.com/google/uuid" "github.com/gorilla/mux" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatWorkspaceHandler struct { service *ChatWorkspaceService } func NewChatWorkspaceHandler(sqlc_q *sqlc_queries.Queries) *ChatWorkspaceHandler { workspaceService := NewChatWorkspaceService(sqlc_q) return &ChatWorkspaceHandler{ service: workspaceService, } } func (h *ChatWorkspaceHandler) Register(router *mux.Router) { router.HandleFunc("/workspaces", h.getWorkspacesByUserID).Methods(http.MethodGet) router.HandleFunc("/workspaces", h.createWorkspace).Methods(http.MethodPost) router.HandleFunc("/workspaces/{uuid}", h.getWorkspaceByUUID).Methods(http.MethodGet) router.HandleFunc("/workspaces/{uuid}", h.updateWorkspace).Methods(http.MethodPut) router.HandleFunc("/workspaces/{uuid}", h.deleteWorkspace).Methods(http.MethodDelete) router.HandleFunc("/workspaces/{uuid}/reorder", h.updateWorkspaceOrder).Methods(http.MethodPut) router.HandleFunc("/workspaces/{uuid}/set-default", h.setDefaultWorkspace).Methods(http.MethodPut) router.HandleFunc("/workspaces/{uuid}/sessions", h.createSessionInWorkspace).Methods(http.MethodPost) router.HandleFunc("/workspaces/{uuid}/sessions", h.getSessionsByWorkspace).Methods(http.MethodGet) router.HandleFunc("/workspaces/default", h.ensureDefaultWorkspace).Methods(http.MethodPost) router.HandleFunc("/workspaces/auto-migrate", h.autoMigrateLegacySessions).Methods(http.MethodPost) } type CreateWorkspaceRequest struct { Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` IsDefault bool `json:"isDefault"` } type UpdateWorkspaceRequest struct { Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` } type UpdateWorkspaceOrderRequest struct { OrderPosition int32 `json:"orderPosition"` } type WorkspaceResponse struct { Uuid string `json:"uuid"` Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` IsDefault bool `json:"isDefault"` OrderPosition int32 `json:"orderPosition"` SessionCount int64 `json:"sessionCount,omitempty"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } // createWorkspace creates a new workspace func (h *ChatWorkspaceHandler) createWorkspace(w http.ResponseWriter, r *http.Request) { var req CreateWorkspaceRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { apiErr := ErrValidationInvalidInput("Invalid request format") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Validate required fields if req.Name == "" { apiErr := ErrValidationInvalidInput("Workspace name is required") RespondWithAPIError(w, apiErr) return } // Default values if req.Color == "" { req.Color = "#6366f1" } if req.Icon == "" { req.Icon = "folder" } ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } workspaceUUID := uuid.New().String() makeDefault := req.IsDefault params := sqlc_queries.CreateWorkspaceParams{ Uuid: workspaceUUID, UserID: userID, Name: req.Name, Description: req.Description, Color: req.Color, Icon: req.Icon, // Set default in a dedicated step to keep exactly one default per user. IsDefault: false, OrderPosition: 0, // Will be updated if needed } workspace, err := h.service.CreateWorkspace(ctx, params) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to create workspace") RespondWithAPIError(w, apiErr) return } if makeDefault { workspace, err = h.setWorkspaceAsDefaultForUser(ctx, userID, workspace.Uuid) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to set default workspace") RespondWithAPIError(w, apiErr) return } } response := WorkspaceResponse{ Uuid: workspace.Uuid, Name: workspace.Name, Description: workspace.Description, Color: workspace.Color, Icon: workspace.Icon, IsDefault: workspace.IsDefault, OrderPosition: workspace.OrderPosition, CreatedAt: workspace.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: workspace.UpdatedAt.Format("2006-01-02T15:04:05Z"), } json.NewEncoder(w).Encode(response) } // getWorkspaceByUUID returns a workspace by its UUID func (h *ChatWorkspaceHandler) getWorkspaceByUUID(w http.ResponseWriter, r *http.Request) { workspaceUUID := mux.Vars(r)["uuid"] log.Printf("🔍 DEBUG: getWorkspaceByUUID called with UUID=%s", workspaceUUID) ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } log.Printf("🔍 DEBUG: getWorkspaceByUUID userID=%d", userID) // Check permission hasPermission, err := h.service.HasWorkspacePermission(ctx, workspaceUUID, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to check workspace permission") RespondWithAPIError(w, apiErr) return } if !hasPermission { apiErr := ErrAuthAccessDenied apiErr.Message = "Access denied to workspace" RespondWithAPIError(w, apiErr) return } workspace, err := h.service.GetWorkspaceByUUID(ctx, workspaceUUID) if err != nil { if err == sql.ErrNoRows { apiErr := ErrResourceNotFound("Workspace") apiErr.Message = "Workspace not found with UUID: " + workspaceUUID RespondWithAPIError(w, apiErr) return } apiErr := WrapError(MapDatabaseError(err), "Failed to get workspace") RespondWithAPIError(w, apiErr) return } response := WorkspaceResponse{ Uuid: workspace.Uuid, Name: workspace.Name, Description: workspace.Description, Color: workspace.Color, Icon: workspace.Icon, IsDefault: workspace.IsDefault, OrderPosition: workspace.OrderPosition, CreatedAt: workspace.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: workspace.UpdatedAt.Format("2006-01-02T15:04:05Z"), } json.NewEncoder(w).Encode(response) } // getWorkspacesByUserID returns all workspaces for the authenticated user func (h *ChatWorkspaceHandler) getWorkspacesByUserID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } workspaces, err := h.service.GetWorkspaceWithSessionCount(ctx, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to get workspaces") RespondWithAPIError(w, apiErr) return } responses := make([]WorkspaceResponse, 0) for _, workspace := range workspaces { response := WorkspaceResponse{ Uuid: workspace.Uuid, Name: workspace.Name, Description: workspace.Description, Color: workspace.Color, Icon: workspace.Icon, IsDefault: workspace.IsDefault, OrderPosition: workspace.OrderPosition, SessionCount: workspace.SessionCount, CreatedAt: workspace.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: workspace.UpdatedAt.Format("2006-01-02T15:04:05Z"), } responses = append(responses, response) } json.NewEncoder(w).Encode(responses) } // updateWorkspace updates an existing workspace func (h *ChatWorkspaceHandler) updateWorkspace(w http.ResponseWriter, r *http.Request) { workspaceUUID := mux.Vars(r)["uuid"] var req UpdateWorkspaceRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { apiErr := ErrValidationInvalidInput("Invalid request format") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Check permission hasPermission, err := h.service.HasWorkspacePermission(ctx, workspaceUUID, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to check workspace permission") RespondWithAPIError(w, apiErr) return } if !hasPermission { apiErr := ErrAuthAccessDenied apiErr.Message = "Access denied to workspace" RespondWithAPIError(w, apiErr) return } params := sqlc_queries.UpdateWorkspaceParams{ Uuid: workspaceUUID, Name: req.Name, Description: req.Description, Color: req.Color, Icon: req.Icon, } workspace, err := h.service.UpdateWorkspace(ctx, params) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to update workspace") RespondWithAPIError(w, apiErr) return } response := WorkspaceResponse{ Uuid: workspace.Uuid, Name: workspace.Name, Description: workspace.Description, Color: workspace.Color, Icon: workspace.Icon, IsDefault: workspace.IsDefault, OrderPosition: workspace.OrderPosition, CreatedAt: workspace.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: workspace.UpdatedAt.Format("2006-01-02T15:04:05Z"), } json.NewEncoder(w).Encode(response) } // updateWorkspaceOrder updates the order position of a workspace func (h *ChatWorkspaceHandler) updateWorkspaceOrder(w http.ResponseWriter, r *http.Request) { workspaceUUID := mux.Vars(r)["uuid"] var req UpdateWorkspaceOrderRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { apiErr := ErrValidationInvalidInput("Invalid request format") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Check permission hasPermission, err := h.service.HasWorkspacePermission(ctx, workspaceUUID, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to check workspace permission") RespondWithAPIError(w, apiErr) return } if !hasPermission { apiErr := ErrAuthAccessDenied apiErr.Message = "Access denied to workspace" RespondWithAPIError(w, apiErr) return } params := sqlc_queries.UpdateWorkspaceOrderParams{ Uuid: workspaceUUID, OrderPosition: req.OrderPosition, } workspace, err := h.service.UpdateWorkspaceOrder(ctx, params) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to update workspace order") RespondWithAPIError(w, apiErr) return } response := WorkspaceResponse{ Uuid: workspace.Uuid, Name: workspace.Name, Description: workspace.Description, Color: workspace.Color, Icon: workspace.Icon, IsDefault: workspace.IsDefault, OrderPosition: workspace.OrderPosition, CreatedAt: workspace.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: workspace.UpdatedAt.Format("2006-01-02T15:04:05Z"), } json.NewEncoder(w).Encode(response) } // deleteWorkspace deletes a workspace func (h *ChatWorkspaceHandler) deleteWorkspace(w http.ResponseWriter, r *http.Request) { workspaceUUID := mux.Vars(r)["uuid"] ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Check permission hasPermission, err := h.service.HasWorkspacePermission(ctx, workspaceUUID, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to check workspace permission") RespondWithAPIError(w, apiErr) return } if !hasPermission { apiErr := ErrAuthAccessDenied apiErr.Message = "Access denied to workspace" RespondWithAPIError(w, apiErr) return } // Get workspace to check if it's default workspace, err := h.service.GetWorkspaceByUUID(ctx, workspaceUUID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to get workspace") RespondWithAPIError(w, apiErr) return } // Prevent deletion of default workspace if workspace.IsDefault { apiErr := ErrValidationInvalidInput("Cannot delete default workspace") RespondWithAPIError(w, apiErr) return } err = h.service.DeleteWorkspace(ctx, workspaceUUID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to delete workspace") RespondWithAPIError(w, apiErr) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "Workspace deleted successfully"}) } // setDefaultWorkspace sets a workspace as the default func (h *ChatWorkspaceHandler) setDefaultWorkspace(w http.ResponseWriter, r *http.Request) { workspaceUUID := mux.Vars(r)["uuid"] ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Check permission hasPermission, err := h.service.HasWorkspacePermission(ctx, workspaceUUID, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to check workspace permission") RespondWithAPIError(w, apiErr) return } if !hasPermission { apiErr := ErrAuthAccessDenied apiErr.Message = "Access denied to workspace" RespondWithAPIError(w, apiErr) return } workspace, err := h.setWorkspaceAsDefaultForUser(ctx, userID, workspaceUUID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to set default workspace") RespondWithAPIError(w, apiErr) return } response := WorkspaceResponse{ Uuid: workspace.Uuid, Name: workspace.Name, Description: workspace.Description, Color: workspace.Color, Icon: workspace.Icon, IsDefault: workspace.IsDefault, OrderPosition: workspace.OrderPosition, CreatedAt: workspace.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: workspace.UpdatedAt.Format("2006-01-02T15:04:05Z"), } json.NewEncoder(w).Encode(response) } func (h *ChatWorkspaceHandler) setWorkspaceAsDefaultForUser(ctx context.Context, userID int32, workspaceUUID string) (sqlc_queries.ChatWorkspace, error) { workspaces, err := h.service.GetWorkspacesByUserID(ctx, userID) if err != nil { return sqlc_queries.ChatWorkspace{}, err } for _, ws := range workspaces { if ws.IsDefault && ws.Uuid != workspaceUUID { _, err = h.service.SetDefaultWorkspace(ctx, sqlc_queries.SetDefaultWorkspaceParams{ Uuid: ws.Uuid, IsDefault: false, }) if err != nil { return sqlc_queries.ChatWorkspace{}, err } } } return h.service.SetDefaultWorkspace(ctx, sqlc_queries.SetDefaultWorkspaceParams{ Uuid: workspaceUUID, IsDefault: true, }) } // ensureDefaultWorkspace ensures the user has a default workspace func (h *ChatWorkspaceHandler) ensureDefaultWorkspace(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } workspace, err := h.service.EnsureDefaultWorkspaceExists(ctx, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to ensure default workspace") RespondWithAPIError(w, apiErr) return } response := WorkspaceResponse{ Uuid: workspace.Uuid, Name: workspace.Name, Description: workspace.Description, Color: workspace.Color, Icon: workspace.Icon, IsDefault: workspace.IsDefault, OrderPosition: workspace.OrderPosition, CreatedAt: workspace.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: workspace.UpdatedAt.Format("2006-01-02T15:04:05Z"), } json.NewEncoder(w).Encode(response) } type CreateSessionInWorkspaceRequest struct { Topic string `json:"topic"` Model string `json:"model"` DefaultSystemPrompt string `json:"defaultSystemPrompt"` } // createSessionInWorkspace creates a new session in a specific workspace func (h *ChatWorkspaceHandler) createSessionInWorkspace(w http.ResponseWriter, r *http.Request) { workspaceUUID := mux.Vars(r)["uuid"] var req CreateSessionInWorkspaceRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { apiErr := ErrValidationInvalidInput("Invalid request format") apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Check workspace permission hasPermission, err := h.service.HasWorkspacePermission(ctx, workspaceUUID, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to check workspace permission") RespondWithAPIError(w, apiErr) return } if !hasPermission { apiErr := ErrAuthAccessDenied apiErr.Message = "Access denied to workspace" RespondWithAPIError(w, apiErr) return } // Get workspace workspace, err := h.service.GetWorkspaceByUUID(ctx, workspaceUUID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to get workspace") RespondWithAPIError(w, apiErr) return } // Create session sessionUUID := uuid.New().String() sessionService := NewChatSessionService(h.service.q) activeSessionService := NewUserActiveChatSessionService(h.service.q) sessionParams := sqlc_queries.CreateChatSessionInWorkspaceParams{ UserID: userID, Uuid: sessionUUID, Topic: req.Topic, CreatedAt: time.Now(), Active: true, MaxLength: 10, Model: req.Model, WorkspaceID: sql.NullInt32{Int32: workspace.ID, Valid: true}, } session, err := sessionService.q.CreateChatSessionInWorkspace(ctx, sessionParams) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to create session in workspace") RespondWithAPIError(w, apiErr) return } _, err = sessionService.EnsureDefaultSystemPrompt(ctx, session.Uuid, userID, req.DefaultSystemPrompt) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to create default system prompt") RespondWithAPIError(w, apiErr) return } // Set as active session (use unified approach) _, err = activeSessionService.UpsertActiveSession(ctx, userID, &workspace.ID, sessionUUID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to set active session") RespondWithAPIError(w, apiErr) return } json.NewEncoder(w).Encode(map[string]interface{}{ "uuid": session.Uuid, "topic": session.Topic, "model": session.Model, "artifactEnabled": session.ArtifactEnabled, "workspaceUuid": workspaceUUID, "createdAt": session.CreatedAt.Format("2006-01-02T15:04:05Z"), }) } // getSessionsByWorkspace returns all sessions in a specific workspace func (h *ChatWorkspaceHandler) getSessionsByWorkspace(w http.ResponseWriter, r *http.Request) { workspaceUUID := mux.Vars(r)["uuid"] ctx := r.Context() userID, err := getUserID(ctx) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } // Check workspace permission hasPermission, err := h.service.HasWorkspacePermission(ctx, workspaceUUID, userID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to check workspace permission") RespondWithAPIError(w, apiErr) return } if !hasPermission { apiErr := ErrAuthAccessDenied apiErr.Message = "Access denied to workspace" RespondWithAPIError(w, apiErr) return } // Get workspace workspace, err := h.service.GetWorkspaceByUUID(ctx, workspaceUUID) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to get workspace") RespondWithAPIError(w, apiErr) return } // Get sessions in workspace sessionService := NewChatSessionService(h.service.q) sessions, err := sessionService.q.GetSessionsByWorkspaceID(ctx, sql.NullInt32{Int32: workspace.ID, Valid: true}) if err != nil { apiErr := WrapError(MapDatabaseError(err), "Failed to get sessions") RespondWithAPIError(w, apiErr) return } sessionResponses := make([]map[string]interface{}, 0) for _, session := range sessions { sessionResponse := map[string]interface{}{ "uuid": session.Uuid, "title": session.Topic, // Use "title" to match the original API "isEdit": false, "model": session.Model, "workspaceUuid": workspaceUUID, "maxLength": session.MaxLength, "temperature": session.Temperature, "maxTokens": session.MaxTokens, "topP": session.TopP, "n": session.N, "debug": session.Debug, "summarizeMode": session.SummarizeMode, "exploreMode": session.ExploreMode, "artifactEnabled": session.ArtifactEnabled, "createdAt": session.CreatedAt.Format("2006-01-02T15:04:05Z"), "updatedAt": session.UpdatedAt.Format("2006-01-02T15:04:05Z"), } sessionResponses = append(sessionResponses, sessionResponse) } json.NewEncoder(w).Encode(sessionResponses) } // autoMigrateLegacySessions automatically detects and migrates legacy sessions without workspace_id func (h *ChatWorkspaceHandler) autoMigrateLegacySessions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, err := getUserID(ctx) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithDebugInfo(err.Error())) return } // Check if user has any legacy sessions (sessions without workspace_id) sessionService := NewChatSessionService(h.service.q) legacySessions, err := sessionService.q.GetSessionsWithoutWorkspace(ctx, userID) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to check for legacy sessions")) return } response := map[string]interface{}{ "hasLegacySessions": len(legacySessions) > 0, "migratedSessions": 0, } // If no legacy sessions, return early if len(legacySessions) == 0 { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) return } // Ensure default workspace exists defaultWorkspace, err := h.service.EnsureDefaultWorkspaceExists(ctx, userID) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to ensure default workspace")) return } // Migrate all legacy sessions to default workspace err = h.service.MigrateSessionsToDefaultWorkspace(ctx, userID, defaultWorkspace.ID) if err != nil { RespondWithAPIError(w, WrapError(MapDatabaseError(err), "Failed to migrate legacy sessions")) return } // Also migrate any legacy active sessions activeSessionService := NewUserActiveChatSessionService(h.service.q) legacyActiveSessions, err := activeSessionService.q.GetAllUserActiveSessions(ctx, userID) if err == nil { for _, activeSession := range legacyActiveSessions { if !activeSession.WorkspaceID.Valid { // This is a legacy global active session, migrate it to default workspace _, _ = activeSessionService.UpsertActiveSession(ctx, userID, &defaultWorkspace.ID, activeSession.ChatSessionUuid) // Delete the old global active session by setting workspace to NULL and deleting _ = activeSessionService.DeleteActiveSession(ctx, userID, nil) } } } response["migratedSessions"] = len(legacySessions) response["defaultWorkspace"] = WorkspaceResponse{ Uuid: defaultWorkspace.Uuid, Name: defaultWorkspace.Name, Description: defaultWorkspace.Description, Color: defaultWorkspace.Color, Icon: defaultWorkspace.Icon, IsDefault: defaultWorkspace.IsDefault, OrderPosition: defaultWorkspace.OrderPosition, CreatedAt: defaultWorkspace.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: defaultWorkspace.UpdatedAt.Format("2006-01-02T15:04:05Z"), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } ================================================ FILE: api/chat_workspace_service.go ================================================ package main import ( "context" "database/sql" "errors" "log" "github.com/google/uuid" "github.com/rotisserie/eris" "github.com/swuecho/chat_backend/sqlc_queries" ) // ChatWorkspaceService provides methods for interacting with chat workspaces. type ChatWorkspaceService struct { q *sqlc_queries.Queries } // NewChatWorkspaceService creates a new ChatWorkspaceService. func NewChatWorkspaceService(q *sqlc_queries.Queries) *ChatWorkspaceService { return &ChatWorkspaceService{q: q} } // CreateWorkspace creates a new workspace. func (s *ChatWorkspaceService) CreateWorkspace(ctx context.Context, params sqlc_queries.CreateWorkspaceParams) (sqlc_queries.ChatWorkspace, error) { workspace, err := s.q.CreateWorkspace(ctx, params) if err != nil { return sqlc_queries.ChatWorkspace{}, eris.Wrap(err, "failed to create workspace") } return workspace, nil } // GetWorkspaceByUUID returns a workspace by UUID. func (s *ChatWorkspaceService) GetWorkspaceByUUID(ctx context.Context, workspaceUUID string) (sqlc_queries.ChatWorkspace, error) { workspace, err := s.q.GetWorkspaceByUUID(ctx, workspaceUUID) if err != nil { return sqlc_queries.ChatWorkspace{}, eris.Wrap(err, "failed to retrieve workspace") } return workspace, nil } // GetWorkspacesByUserID returns all workspaces for a user. func (s *ChatWorkspaceService) GetWorkspacesByUserID(ctx context.Context, userID int32) ([]sqlc_queries.ChatWorkspace, error) { workspaces, err := s.q.GetWorkspacesByUserID(ctx, userID) if err != nil { return nil, eris.Wrap(err, "failed to retrieve workspaces") } return workspaces, nil } // GetWorkspaceWithSessionCount returns all workspaces with session counts for a user. func (s *ChatWorkspaceService) GetWorkspaceWithSessionCount(ctx context.Context, userID int32) ([]sqlc_queries.GetWorkspaceWithSessionCountRow, error) { workspaces, err := s.q.GetWorkspaceWithSessionCount(ctx, userID) if err != nil { return nil, eris.Wrap(err, "failed to retrieve workspaces with session count") } return workspaces, nil } // UpdateWorkspace updates an existing workspace. func (s *ChatWorkspaceService) UpdateWorkspace(ctx context.Context, params sqlc_queries.UpdateWorkspaceParams) (sqlc_queries.ChatWorkspace, error) { workspace, err := s.q.UpdateWorkspace(ctx, params) if err != nil { return sqlc_queries.ChatWorkspace{}, eris.Wrap(err, "failed to update workspace") } return workspace, nil } // UpdateWorkspaceOrder updates the order position of a workspace. func (s *ChatWorkspaceService) UpdateWorkspaceOrder(ctx context.Context, params sqlc_queries.UpdateWorkspaceOrderParams) (sqlc_queries.ChatWorkspace, error) { workspace, err := s.q.UpdateWorkspaceOrder(ctx, params) if err != nil { return sqlc_queries.ChatWorkspace{}, eris.Wrap(err, "failed to update workspace order") } return workspace, nil } // DeleteWorkspace deletes a workspace by UUID. func (s *ChatWorkspaceService) DeleteWorkspace(ctx context.Context, workspaceUUID string) error { err := s.q.DeleteWorkspace(ctx, workspaceUUID) if err != nil { return eris.Wrap(err, "failed to delete workspace") } return nil } // GetDefaultWorkspaceByUserID returns the default workspace for a user. func (s *ChatWorkspaceService) GetDefaultWorkspaceByUserID(ctx context.Context, userID int32) (sqlc_queries.ChatWorkspace, error) { workspace, err := s.q.GetDefaultWorkspaceByUserID(ctx, userID) if err != nil { return sqlc_queries.ChatWorkspace{}, eris.Wrap(err, "failed to retrieve default workspace") } return workspace, nil } // SetDefaultWorkspace sets a workspace as the default. func (s *ChatWorkspaceService) SetDefaultWorkspace(ctx context.Context, params sqlc_queries.SetDefaultWorkspaceParams) (sqlc_queries.ChatWorkspace, error) { workspace, err := s.q.SetDefaultWorkspace(ctx, params) if err != nil { return sqlc_queries.ChatWorkspace{}, eris.Wrap(err, "failed to set default workspace") } return workspace, nil } // CreateDefaultWorkspace creates a default workspace for a user. func (s *ChatWorkspaceService) CreateDefaultWorkspace(ctx context.Context, userID int32) (sqlc_queries.ChatWorkspace, error) { workspaceUUID := uuid.New().String() params := sqlc_queries.CreateDefaultWorkspaceParams{ Uuid: workspaceUUID, UserID: userID, } workspace, err := s.q.CreateDefaultWorkspace(ctx, params) if err != nil { return sqlc_queries.ChatWorkspace{}, eris.Wrap(err, "failed to create default workspace") } return workspace, nil } // EnsureDefaultWorkspaceExists ensures a user has a default workspace, creating one if needed. func (s *ChatWorkspaceService) EnsureDefaultWorkspaceExists(ctx context.Context, userID int32) (sqlc_queries.ChatWorkspace, error) { // Try to get existing default workspace workspace, err := s.GetDefaultWorkspaceByUserID(ctx, userID) if err != nil { // If no default workspace exists, create one if errors.Is(err, sql.ErrNoRows) { return s.CreateDefaultWorkspace(ctx, userID) } return sqlc_queries.ChatWorkspace{}, err } return workspace, nil } // HasWorkspacePermission checks if a user has permission to access a workspace. func (s *ChatWorkspaceService) HasWorkspacePermission(ctx context.Context, workspaceUUID string, userID int32) (bool, error) { log.Printf("🔍 DEBUG: Checking permission for workspace=%s, user=%d", workspaceUUID, userID) result, err := s.q.HasWorkspacePermission(ctx, sqlc_queries.HasWorkspacePermissionParams{ Uuid: workspaceUUID, UserID: userID, }) if err != nil { log.Printf("❌ DEBUG: Permission check failed: %v", err) return false, eris.Wrap(err, "failed to check workspace permission") } log.Printf("✅ DEBUG: Permission result=%t for workspace=%s, user=%d", result, workspaceUUID, userID) return result, nil } // MigrateSessionsToDefaultWorkspace migrates all sessions without workspace to default workspace. func (s *ChatWorkspaceService) MigrateSessionsToDefaultWorkspace(ctx context.Context, userID int32, workspaceID int32) error { err := s.q.MigrateSessionsToDefaultWorkspace(ctx, sqlc_queries.MigrateSessionsToDefaultWorkspaceParams{ UserID: userID, WorkspaceID: sql.NullInt32{Int32: workspaceID, Valid: true}, }) if err != nil { return eris.Wrap(err, "failed to migrate sessions to default workspace") } return nil } ================================================ FILE: api/constants.go ================================================ // Package main provides constants used throughout the chat application. // This file contains all magic numbers, timeouts, and configuration values // to improve code maintainability and avoid scattered constants. package main import "time" // API and request constants const ( // Timeout settings DefaultRequestTimeout = 5 * time.Minute // Loop limits and safety guards MaxStreamingLoopIterations = 10000 // Content buffering and flushing SmallAnswerThreshold = 200 FlushCharacterThreshold = 500 TestPrefixLength = 16 // Pagination DefaultPageSize = 200 MaxHistoryItems = 10000 // Rate limiting DefaultPageLimit = 30 // Test constants TestDemoPrefix = "test_demo_bestqa" // Service constants DefaultMaxLength = 10 DefaultTemperature = 0.7 DefaultMaxTokens = 4096 DefaultTopP = 1.0 DefaultN = 1 RequestTimeoutSeconds = 10 TokenEstimateRatio = 4 SummarizeThreshold = 300 DefaultSystemPromptText = "You are a helpful, concise assistant. Ask clarifying questions when needed. Provide accurate answers with short reasoning and actionable steps. If unsure, say so and suggest how to verify." ) // Error message constants const ( ErrorStreamUnsupported = "Streaming unsupported by client" ErrorNoContent = "no content in answer" ErrorEndOfStream = "End of stream reached" ErrorDoneBreak = "DONE break" ) // HTTP constants const ( ContentTypeJSON = "application/json" AcceptEventStream = "text/event-stream" CacheControlNoCache = "no-cache" ConnectionKeepAlive = "keep-alive" ) ================================================ FILE: api/embed_debug_test.go ================================================ package main import "testing" func TestEmbedInstructions(t *testing.T) { if artifactInstructionText == "" { t.Fatalf("artifactInstructionText is empty") } } ================================================ FILE: api/errors.go ================================================ package main import ( "context" "database/sql" "encoding/json" "errors" "fmt" "log" "net/http" "sort" "strings" "github.com/jackc/pgconn" ) // APIError represents a standardized error response for the API // It includes both user-facing and internal debugging information type APIError struct { HTTPCode int `json:"-"` // HTTP status code (not exposed in response) Code string `json:"code"` // Application-specific error code following format: DOMAIN_NNN Message string `json:"message"` // Human-readable message for end users Detail string `json:"detail,omitempty"` // Optional error details for debugging DebugInfo string `json:"-"` // Internal debugging info (not exposed in responses) } // NewAPIError creates a new APIError with the given parameters func NewAPIError(httpCode int, code, message string) APIError { return APIError{ HTTPCode: httpCode, Code: code, Message: message, } } // withMessage adds a message to an APIError func (e APIError) WithMessage(message string) APIError { e.Message = message return e } // WithMessage adds detail to an APIError func (e APIError) WithDetail(detail string) APIError { e.Detail = detail return e } // WithDebugInfo adds debug info to an APIError func (e APIError) WithDebugInfo(debugInfo string) APIError { e.DebugInfo = debugInfo return e } func (e APIError) Error() string { return fmt.Sprintf("[%s] %s %s", e.Code, e.Message, e.Detail) } // Error code prefixes by domain const ( ErrAuth = "AUTH" // Authentication/Authorization errors (100-199) ErrValidation = "VALD" // Validation errors (200-299) ErrResource = "RES" // Resource-related errors (300-399) ErrDatabase = "DB" // Database errors (400-499) ErrExternal = "EXT" // External service errors (500-599) ErrInternal = "INTN" // Internal application errors (600-699) ErrModel = "MODEL" // Model related errors (700-799) ) // Error code ranges: // - Each domain has 100 codes available (000-099) // - Codes should be sequential within each domain // - New errors should use the next available code in their domain // Define external service errors var ( ErrExternalTimeout = APIError{ HTTPCode: http.StatusGatewayTimeout, Code: ErrExternal + "_001", Message: "External service timed out", } ErrExternalUnavailable = APIError{ HTTPCode: http.StatusServiceUnavailable, Code: ErrExternal + "_002", Message: "External service unavailable", } ) // Define all API errors var ( // Auth errors ErrAuthInvalidCredentials = APIError{ HTTPCode: http.StatusUnauthorized, Code: ErrAuth + "_001", Message: "Invalid credentials", } ErrAuthExpiredToken = APIError{ HTTPCode: http.StatusUnauthorized, Code: ErrAuth + "_002", Message: "Token has expired", } ErrAuthAdminRequired = APIError{ HTTPCode: http.StatusForbidden, Code: ErrAuth + "_003", Message: "Admin privileges required", } ErrAuthInvalidEmailOrPassword = APIError{ HTTPCode: http.StatusForbidden, Code: ErrAuth + "_004", Message: "invalid email or password", } ErrAuthAccessDenied = APIError{ HTTPCode: http.StatusForbidden, Code: ErrAuth + "_005", Message: "Access denied", } // Resource errors ErrResourceNotFoundGeneric = APIError{ HTTPCode: http.StatusNotFound, Code: ErrResource + "_001", Message: "Resource not found", } ErrResourceAlreadyExistsGeneric = APIError{ HTTPCode: http.StatusConflict, Code: ErrResource + "_002", Message: "Resource already exists", } ErrChatSessionNotFound = APIError{ HTTPCode: http.StatusNotFound, Code: ErrResource + "_004", Message: "Chat session not found", } ErrChatMessageNotFound = APIError{ HTTPCode: http.StatusNotFound, Code: ErrResource + "_007", Message: "Chat message not found", } ErrChatStreamFailed = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrInternal + "_004", Message: "Failed to stream chat response", } ErrChatRequestFailed = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrInternal + "_005", Message: "Failed to make chat request", } ErrChatFileNotFound = APIError{ HTTPCode: http.StatusNotFound, Code: ErrResource + "_005", Message: "Chat file not found", } ErrChatModelNotFound = APIError{ HTTPCode: http.StatusNotFound, Code: ErrResource + "_006", Message: "Chat model not found", } ErrChatFileTooLarge = APIError{ HTTPCode: http.StatusBadRequest, Code: ErrValidation + "_002", Message: "File too large", } ErrChatFileInvalidType = APIError{ HTTPCode: http.StatusBadRequest, Code: ErrValidation + "_003", Message: "Invalid file type", } ErrChatSessionInvalid = APIError{ HTTPCode: http.StatusBadRequest, Code: ErrValidation + "_004", Message: "Invalid chat session", } ErrTooManyRequests = APIError{ HTTPCode: http.StatusTooManyRequests, Code: ErrResource + "_003", Message: "Rate limit exceeded", Detail: "Too many requests in the given time period", } // Validation errors ErrValidationInvalidInputGeneric = APIError{ HTTPCode: http.StatusBadRequest, Code: ErrValidation + "_001", Message: "Invalid input", } // Database errors ErrDatabaseQuery = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrDatabase + "_001", Message: "Database query failed", DebugInfo: "Database operation failed - check logs for details", } ErrDatabaseConnection = APIError{ HTTPCode: http.StatusServiceUnavailable, Code: ErrDatabase + "_002", Message: "Database connection failed", DebugInfo: "Could not connect to database - check connection settings", } ErrDatabaseForeignKey = APIError{ HTTPCode: http.StatusBadRequest, Code: ErrDatabase + "_003", Message: "Referenced resource does not exist", DebugInfo: "Foreign key violation", } // model related errors ErrSystemMessageError = APIError{ HTTPCode: http.StatusBadRequest, Code: ErrModel + "_001", Message: "Usage error, system message input, not user input", } ErrClaudeStreamFailed = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrModel + "_002", Message: "Failed to stream Claude response", } ErrClaudeRequestFailed = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrModel + "_003", Message: "Failed to make Claude request", } ErrClaudeInvalidResponse = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrModel + "_004", Message: "Invalid response from Claude API", } ErrClaudeResponseFaild = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrModel + "_005", Message: "Failed to stream Claude response", } // OpenAI specific errors ErrOpenAIStreamFailed = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrModel + "_006", Message: "Failed to stream OpenAI response", } ErrOpenAIRequestFailed = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrModel + "_007", Message: "Failed to make OpenAI request", } ErrOpenAIInvalidResponse = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrModel + "_008", Message: "Invalid response from OpenAI API", } ErrOpenAIConfigFailed = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrModel + "_009", Message: "Failed to configure OpenAI client", } // Internal errors ErrInternalUnexpected = APIError{ HTTPCode: http.StatusInternalServerError, Code: ErrInternal + "_001", Message: "An unexpected error occurred", DebugInfo: "Unexpected internal error - check logs for stack trace", } ) // Helper functions to create specific errors with dynamic content func ErrResourceNotFound(resource string) APIError { err := ErrResourceNotFoundGeneric err.Message = resource + " not found" return err } func ErrResourceAlreadyExists(resource string) APIError { err := ErrResourceAlreadyExistsGeneric err.Message = resource + " already exists" return err } func ErrValidationInvalidInput(detail string) APIError { err := ErrValidationInvalidInputGeneric err.Detail = detail return err } // RespondWithAPIError writes an APIError response to the client // It: // - Sets the appropriate HTTP status code // - Returns a JSON response with error details // - Logs the error with debug info func RespondWithAPIError(w http.ResponseWriter, err APIError) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(err.HTTPCode) // Error response structure response := struct { Code string `json:"code"` // Application error code Message string `json:"message"` // Human-readable error message Detail string `json:"detail,omitempty"` // Additional error details }{ Code: err.Code, Message: err.Message, Detail: err.Detail + " " + err.DebugInfo, } // Log error with debug info if available if err.DebugInfo != "" { log.Printf("Error [%s]: %s - %s", err.Code, err.Message, err.DebugInfo) } // Write JSON response if err := json.NewEncoder(w).Encode(response); err != nil { log.Printf("Failed to write error response: %v", err) } } func MapDatabaseError(err error) error { // Map common database errors to appropriate application errors if errors.Is(err, sql.ErrNoRows) { return ErrResourceNotFound("Record") } // Check for connection errors if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "no such host") || strings.Contains(err.Error(), "connection reset by peer") { dbErr := ErrDatabaseConnection dbErr.DebugInfo = err.Error() return dbErr } // Check for other specific database errors var pgErr *pgconn.PgError if errors.As(err, &pgErr) { switch pgErr.Code { case "23505": // Unique violation return ErrResourceAlreadyExists("Record") case "23503": // Foreign key violation dbErr := ErrDatabaseForeignKey dbErr.DebugInfo = fmt.Sprintf("Foreign key violation: %s", pgErr.Detail) return dbErr case "42P01": // Undefined table dbErr := ErrDatabaseQuery dbErr.Message = "Database schema error" dbErr.DebugInfo = fmt.Sprintf("Table does not exist: %s", pgErr.Detail) return dbErr case "42703": // Undefined column dbErr := ErrDatabaseQuery dbErr.Message = "Database schema error" dbErr.DebugInfo = fmt.Sprintf("Column does not exist: %s", pgErr.Detail) return dbErr case "53300": // Too many connections dbErr := ErrDatabaseConnection dbErr.Message = "Database connection limit reached" dbErr.DebugInfo = pgErr.Detail return dbErr } } // Log the unhandled database error log.Printf("Unhandled database error: %v", err) // Return generic database error dbErr := ErrDatabaseQuery dbErr.DebugInfo = err.Error() return dbErr } // ErrorCatalog holds all error codes for documentation purposes var ErrorCatalog = map[string]APIError{ // Auth errors ErrAuthInvalidCredentials.Code: ErrAuthInvalidCredentials, ErrAuthExpiredToken.Code: ErrAuthExpiredToken, ErrAuthAdminRequired.Code: ErrAuthAdminRequired, // Resource errors ErrResourceNotFoundGeneric.Code: ErrResourceNotFoundGeneric, ErrResourceAlreadyExistsGeneric.Code: ErrResourceAlreadyExistsGeneric, ErrTooManyRequests.Code: ErrTooManyRequests, // Validation errors ErrValidationInvalidInputGeneric.Code: ErrValidationInvalidInputGeneric, // Database errors ErrDatabaseQuery.Code: ErrDatabaseQuery, ErrDatabaseConnection.Code: ErrDatabaseConnection, ErrDatabaseForeignKey.Code: ErrDatabaseForeignKey, // External service errors ErrExternalTimeout.Code: ErrExternalTimeout, ErrExternalUnavailable.Code: ErrExternalUnavailable, // External service errors ErrExternalTimeout.Code: ErrExternalTimeout, ErrExternalUnavailable.Code: ErrExternalUnavailable, // Internal errors ErrInternalUnexpected.Code: ErrInternalUnexpected, ErrInternal + "_002": {HTTPCode: http.StatusGatewayTimeout, Code: ErrInternal + "_002", Message: "Request timed out"}, ErrInternal + "_003": {HTTPCode: http.StatusRequestTimeout, Code: ErrInternal + "_003", Message: "Request was canceled"}, ErrInternal + "_004": ErrChatStreamFailed, ErrInternal + "_005": ErrChatRequestFailed, ErrResource + "_004": ErrChatSessionNotFound, ErrResource + "_005": ErrChatFileNotFound, ErrResource + "_006": ErrChatModelNotFound, ErrResource + "_007": ErrChatMessageNotFound, // model related errors ErrModel + "_001": ErrSystemMessageError, ErrModel + "_002": ErrClaudeStreamFailed, ErrModel + "_003": ErrClaudeRequestFailed, ErrModel + "_004": ErrClaudeInvalidResponse, ErrModel + "_005": ErrClaudeResponseFaild, ErrModel + "_006": ErrOpenAIStreamFailed, ErrModel + "_007": ErrOpenAIRequestFailed, ErrModel + "_008": ErrOpenAIInvalidResponse, ErrModel + "_009": ErrOpenAIConfigFailed, } // WrapError converts a standard error into an APIError // It handles: // - Context cancellation/timeout errors // - Existing APIErrors (preserves original error details) // - Unknown errors (converts to internal server error) // Parameters: // - err: The original error to wrap // - detail: Additional context about where the error occurred // // Returns: // - APIError: A standardized error response func WrapError(err error, detail string) APIError { var apiErr APIError // Handle context errors switch { case errors.Is(err, context.DeadlineExceeded): apiErr = APIError{ HTTPCode: http.StatusGatewayTimeout, Code: ErrInternal + "_002", Message: "Request timed out", Detail: detail, DebugInfo: "Context deadline exceeded", } return apiErr case errors.Is(err, context.Canceled): apiErr = APIError{ HTTPCode: http.StatusRequestTimeout, Code: ErrInternal + "_003", Message: "Request was canceled", Detail: detail, DebugInfo: "Context was canceled", } return apiErr } // Handle APIError types switch e := err.(type) { case APIError: apiErr = e if detail != "" { if apiErr.Detail != "" { apiErr.Detail = fmt.Sprintf("%s: %s", detail, apiErr.Detail) } else { apiErr.Detail = detail } } default: // Convert unknown errors to internal server error apiErr = ErrInternalUnexpected apiErr.Detail = detail apiErr.DebugInfo = err.Error() } return apiErr } // IsErrorCode checks if an error is an APIError with the specified code func IsErrorCode(err error, code string) bool { if apiErr, ok := err.(APIError); ok { return apiErr.Code == code } return false } // Add a handler to serve the error catalog func ErrorCatalogHandler(w http.ResponseWriter, r *http.Request) { type ErrorDoc struct { Code string `json:"code"` HTTPCode int `json:"http_code"` Message string `json:"message"` } docs := make([]ErrorDoc, 0, len(ErrorCatalog)) for code, info := range ErrorCatalog { docs = append(docs, ErrorDoc{ Code: code, HTTPCode: info.HTTPCode, Message: info.Message, }) } // Sort by error code sort.Slice(docs, func(i, j int) bool { return docs[i].Code < docs[j].Code }) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(docs) } // createAPIError creates a consistent API error with optional debug info func createAPIError(baseErr APIError, detail string, debugInfo string) APIError { apiErr := baseErr if detail != "" { apiErr.Detail = detail } if debugInfo != "" { apiErr.DebugInfo = debugInfo } return apiErr } ================================================ FILE: api/file_upload_handler.go ================================================ package main import ( "bytes" "database/sql" "encoding/json" "errors" "fmt" "io" "log" "net/http" "strconv" "strings" "github.com/gorilla/mux" "github.com/swuecho/chat_backend/sqlc_queries" ) type ChatFileHandler struct { service *ChatFileService } func NewChatFileHandler(sqlc_q *sqlc_queries.Queries) *ChatFileHandler { ChatFileService := NewChatFileService(sqlc_q) return &ChatFileHandler{ service: ChatFileService, } } func (h *ChatFileHandler) Register(router *mux.Router) { router.HandleFunc("/upload", h.ReceiveFile).Methods(http.MethodPost) router.HandleFunc("/chat_file/{uuid}/list", h.ChatFilesBySessionUUID).Methods(http.MethodGet) router.HandleFunc("/download/{id}", h.DownloadFile).Methods(http.MethodGet) router.HandleFunc("/download/{id}", h.DeleteFile).Methods(http.MethodDelete) } const ( maxUploadSize = 32 << 20 // 32 MB ) var allowedTypes = map[string]string{ "image/jpeg": ".jpg", "image/png": ".png", "application/pdf": ".pdf", "text/plain": ".txt", "application/json": ".json", } // isValidFileType checks if the file type is allowed and matches the extension func isValidFileType(mimeType, fileName string) bool { // Get expected extension for mime type expectedExt, ok := allowedTypes[mimeType] if !ok { return false } // Check if file has the expected extension return strings.HasSuffix(strings.ToLower(fileName), expectedExt) } func (h *ChatFileHandler) ReceiveFile(w http.ResponseWriter, r *http.Request) { // Parse multipart form with size limit if err := r.ParseMultipartForm(maxUploadSize); err != nil { RespondWithAPIError(w, ErrValidationInvalidInput(fmt.Sprintf("file too large, max size is %d bytes", maxUploadSize))) return } // Get session UUID sessionUUID := r.FormValue("session-uuid") if sessionUUID == "" { RespondWithAPIError(w, ErrValidationInvalidInput("missing session UUID")) return } // Get user ID userID, err := getUserID(r.Context()) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } // Get uploaded file file, header, err := r.FormFile("file") if err != nil { RespondWithAPIError(w, WrapError(err, "failed to get uploaded file")) return } defer func() { if err := file.Close(); err != nil { log.Printf("Error closing uploaded file: %v", err) } }() // Validate file type and extension mimeType := header.Header.Get("Content-Type") if !isValidFileType(mimeType, header.Filename) { RespondWithAPIError(w, ErrChatFileInvalidType.WithMessage( fmt.Sprintf("unsupported file type: %s or invalid extension for type", mimeType))) return } log.Printf("Uploading file: %s (%s, %d bytes)", header.Filename, mimeType, header.Size) // Validate file size if header.Size > maxUploadSize { RespondWithAPIError(w, ErrValidationInvalidInput(fmt.Sprintf("file too large, max size is %d bytes", maxUploadSize))) return } // Read file into buffer with size limit var buf bytes.Buffer limitedReader := &io.LimitedReader{R: file, N: maxUploadSize} if _, err := io.Copy(&buf, limitedReader); err != nil { RespondWithAPIError(w, WrapError(err, "failed to read uploaded file")) return } // Check if we hit the size limit if limitedReader.N <= 0 { RespondWithAPIError(w, ErrValidationInvalidInput( fmt.Sprintf("file exceeds maximum size of %d bytes", maxUploadSize))) return } // Create chat file record chatFile, err := h.service.q.CreateChatFile(r.Context(), sqlc_queries.CreateChatFileParams{ ChatSessionUuid: sessionUUID, UserID: userID, Name: header.Filename, Data: buf.Bytes(), MimeType: mimeType, }) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to create chat file record")) return } // Clean up buffer buf.Reset() // Return success response w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{ "url": fmt.Sprintf("/download/%d", chatFile.ID), "name": header.Filename, "type": mimeType, "size": fmt.Sprintf("%d", header.Size), }) } func (h *ChatFileHandler) DownloadFile(w http.ResponseWriter, r *http.Request) { fileID := mux.Vars(r)["id"] fileIdInt, err := strconv.ParseInt(fileID, 10, 32) if err != nil { RespondWithAPIError(w, ErrValidationInvalidInput("invalid file ID")) return } file, err := h.service.q.GetChatFileByID(r.Context(), int32(fileIdInt)) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithAPIError(w, ErrChatFileNotFound.WithMessage(fmt.Sprintf("file ID %d not found", fileIdInt))) } else { RespondWithAPIError(w, WrapError(err, "failed to get chat file")) } return } // Set proper content type from stored mime type w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", file.Name)) w.Header().Set("Content-Length", strconv.Itoa(len(file.Data))) if _, err := w.Write(file.Data); err != nil { log.Printf("Failed to write file data: %v", err) } } func (h *ChatFileHandler) DeleteFile(w http.ResponseWriter, r *http.Request) { fileID := mux.Vars(r)["id"] fileIdInt, _ := strconv.ParseInt(fileID, 10, 32) _, err := h.service.q.DeleteChatFile(r.Context(), int32(fileIdInt)) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to delete chat file")) return } w.WriteHeader(http.StatusOK) } func (h *ChatFileHandler) ChatFilesBySessionUUID(w http.ResponseWriter, r *http.Request) { sessionUUID := mux.Vars(r)["uuid"] userID, err := getUserID(r.Context()) if err != nil { RespondWithAPIError(w, ErrAuthInvalidCredentials.WithMessage("missing or invalid user ID")) return } chatFiles, err := h.service.q.ListChatFilesBySessionUUID(r.Context(), sqlc_queries.ListChatFilesBySessionUUIDParams{ ChatSessionUuid: sessionUUID, UserID: userID, }) if err != nil { RespondWithAPIError(w, WrapError(err, "failed to list chat files for session")) return } w.WriteHeader(http.StatusOK) if len(chatFiles) == 0 { w.Write([]byte("[]")) return } json.NewEncoder(w).Encode(chatFiles) } ================================================ FILE: api/file_upload_service.go ================================================ package main import ( "context" "log" "github.com/swuecho/chat_backend/sqlc_queries" ) // ChatFileService handles operations related to chat file uploads type ChatFileService struct { q *sqlc_queries.Queries } // NewChatFileService creates a new ChatFileService instance func NewChatFileService(q *sqlc_queries.Queries) *ChatFileService { return &ChatFileService{q: q} } // CreateChatUpload handles creating a new chat file upload func (s *ChatFileService) CreateChatUpload(ctx context.Context, params sqlc_queries.CreateChatFileParams) (sqlc_queries.ChatFile, error) { // Validate input if params.ChatSessionUuid == "" { return sqlc_queries.ChatFile{}, ErrValidationInvalidInput("missing session UUID") } if params.UserID <= 0 { return sqlc_queries.ChatFile{}, ErrValidationInvalidInput("invalid user ID") } if params.Name == "" { return sqlc_queries.ChatFile{}, ErrValidationInvalidInput("missing file name") } if len(params.Data) == 0 { return sqlc_queries.ChatFile{}, ErrValidationInvalidInput("empty file data") } log.Printf("Creating chat file upload for session %s, user %d", params.ChatSessionUuid, params.UserID) upload, err := s.q.CreateChatFile(ctx, params) if err != nil { return sqlc_queries.ChatFile{}, WrapError(err, "failed to create chat file") } log.Printf("Created chat file upload ID %d", upload.ID) return upload, nil } // GetChatFile retrieves a chat file by ID func (s *ChatFileService) GetChatFile(ctx context.Context, id int32) (sqlc_queries.GetChatFileByIDRow, error) { if id <= 0 { return sqlc_queries.GetChatFileByIDRow{}, ErrValidationInvalidInput("invalid file ID") } log.Printf("Retrieving chat file ID %d", id) file, err := s.q.GetChatFileByID(ctx, id) if err != nil { return sqlc_queries.GetChatFileByIDRow{}, WrapError(err, "failed to get chat file") } return file, nil } // DeleteChatFile deletes a chat file by ID func (s *ChatFileService) DeleteChatFile(ctx context.Context, id int32) error { if id <= 0 { return ErrValidationInvalidInput("invalid file ID") } log.Printf("Deleting chat file ID %d", id) _, err := s.q.DeleteChatFile(ctx, id) if err != nil { return WrapError(err, "failed to delete chat file") } return nil } // ListChatFilesBySession retrieves chat files for a session func (s *ChatFileService) ListChatFilesBySession(ctx context.Context, sessionUUID string, userID int32) ([]sqlc_queries.ListChatFilesBySessionUUIDRow, error) { if sessionUUID == "" { return nil, ErrValidationInvalidInput("missing session UUID") } if userID <= 0 { return nil, ErrValidationInvalidInput("invalid user ID") } log.Printf("Listing chat files for session %s, user %d", sessionUUID, userID) files, err := s.q.ListChatFilesBySessionUUID(ctx, sqlc_queries.ListChatFilesBySessionUUIDParams{ ChatSessionUuid: sessionUUID, UserID: userID, }) if err != nil { return nil, WrapError(err, "failed to list chat files") } return files, nil } ================================================ FILE: api/go.mod ================================================ module github.com/swuecho/chat_backend go 1.19 require ( github.com/deckarep/golang-set/v2 v2.6.0 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.0 github.com/jackc/pgconn v1.14.3 github.com/lib/pq v1.10.9 github.com/ory/dockertest/v3 v3.10.0 github.com/pkoukk/tiktoken-go v0.1.6 github.com/rotisserie/eris v0.5.4 github.com/samber/lo v1.39.0 github.com/sashabaranov/go-openai v1.36.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.18.2 github.com/tmc/langchaingo v0.0.0-20230610024316-06cb7b57ea80 golang.org/x/crypto v0.23.0 gotest.tools/v3 v3.4.0 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/text v0.15.0 // indirect golang.org/x/tools v0.13.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/continuity v0.3.0 // indirect github.com/docker/cli v23.0.4+incompatible // indirect github.com/docker/docker v23.0.4+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/handlers v1.5.1 github.com/imdario/mergo v0.3.15 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.6 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/time v0.5.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) ================================================ FILE: api/go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v23.0.4+incompatible h1:xClB7PsiATttDHj8ce5qvJcikiApNy7teRR1XkoBZGs= github.com/docker/cli v23.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v23.0.4+incompatible h1:Kd3Bh9V/rO+XpTP/BLqM+gx8z7+Yb0AA2Ibj+nNo4ek= github.com/docker/docker v23.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v1.1.6 h1:XbhB8IfG/EsnhNvZtNdLB0GBw92GYEFvKlhaJk9jUgA= github.com/opencontainers/runc v1.1.6/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sashabaranov/go-openai v1.36.1 h1:EVfRXwIlW2rUzpx6vR+aeIKCK/xylSrVYAx1TMTSX3g= github.com/sashabaranov/go-openai v1.36.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tmc/langchaingo v0.0.0-20230610024316-06cb7b57ea80 h1:Y+a76dNVbdWduw3gznOr2O2OSZkdwDRYPKTDpG/vM9I= github.com/tmc/langchaingo v0.0.0-20230610024316-06cb7b57ea80/go.mod h1:6l1WoyqVDwkv7cFlY3gfcTv8yVowVyuutKv8PGlQCWI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg= go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= ================================================ FILE: api/handle_tts.go ================================================ package main import ( "fmt" "io" "net/http" "os" ) func handleTTSRequest(w http.ResponseWriter, r *http.Request) { // Create a new HTTP request with the same method, URL, and body as the original request targetURL := r.URL hostEnvVarName := "TTS_HOST" portEnvVarName := "TTS_PORT" realHost := fmt.Sprintf("http://%s:%s/api", os.Getenv(hostEnvVarName), os.Getenv(portEnvVarName)) fullURL := realHost + targetURL.String() print(fullURL) proxyReq, err := http.NewRequest(r.Method, fullURL, r.Body) if err != nil { RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Error creating proxy request").WithDebugInfo(err.Error())) return } // Copy the headers from the original request to the proxy request for name, values := range r.Header { for _, value := range values { proxyReq.Header.Add(name, value) } } var customTransport = http.DefaultTransport // Send the proxy request using the custom transport resp, err := customTransport.RoundTrip(proxyReq) if err != nil { RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Error sending proxy request").WithDebugInfo(err.Error())) return } defer resp.Body.Close() // Copy the headers from the proxy response to the original response for name, values := range resp.Header { for _, value := range values { w.Header().Add(name, value) } } // Set the status code of the original response to the status code of the proxy response w.WriteHeader(resp.StatusCode) // Copy the body of the proxy response to the original response io.Copy(w, resp.Body) } ================================================ FILE: api/jwt_secret_service.go ================================================ package main // check if jwt_secret and jwt_aud available for 'chat' in database // if not, create them import ( "context" "database/sql" "errors" "github.com/rotisserie/eris" "github.com/swuecho/chat_backend/auth" "github.com/swuecho/chat_backend/sqlc_queries" ) type JWTSecretService struct { q *sqlc_queries.Queries } // NewJWTSecretService creates a new JWTSecretService. func NewJWTSecretService(q *sqlc_queries.Queries) *JWTSecretService { return &JWTSecretService{q: q} } // GetJWTSecret returns a jwt_secret by name. func (s *JWTSecretService) GetJwtSecret(ctx context.Context, name string) (sqlc_queries.JwtSecret, error) { secret, err := s.q.GetJwtSecret(ctx, name) if err != nil { return sqlc_queries.JwtSecret{}, eris.Wrap(err, "failed to get secret ") } return secret, nil } // GetOrCreateJwtSecret returns a jwt_secret by name. // if jwt_secret does not exist, create it func (s *JWTSecretService) GetOrCreateJwtSecret(ctx context.Context, name string) (sqlc_queries.JwtSecret, error) { secret, err := s.q.GetJwtSecret(ctx, name) if err != nil { // no row found, create it if errors.Is(err, sql.ErrNoRows) { secret_str, aud_str := auth.GenJwtSecretAndAudience() secret, err = s.q.CreateJwtSecret(ctx, sqlc_queries.CreateJwtSecretParams{ Name: name, Secret: secret_str, Audience: aud_str, }) if err != nil { return sqlc_queries.JwtSecret{}, eris.Wrap(err, "failed to create secret ") } } else { return sqlc_queries.JwtSecret{}, eris.Wrap(err, "failed to create secret ") } } return secret, nil } ================================================ FILE: api/llm/claude/claude.go ================================================ package claude import ( "encoding/json" "fmt" "strings" models "github.com/swuecho/chat_backend/models" ) type Delta struct { Type string `json:"type"` Text string `json:"text"` } type ContentBlockDelta struct { Type string `json:"type"` Index int `json:"index"` Delta Delta `json:"delta"` } type ContentBlock struct { Type string `json:"type"` Text string `json:"text"` } type StartBlock struct { Type string `json:"type"` Index int `json:"index"` ContentBlock ContentBlock `json:"content_block"` } func AnswerFromBlockDelta(line []byte) string { var response ContentBlockDelta _ = json.Unmarshal(line, &response) return response.Delta.Text } func AnswerFromBlockStart(line []byte) string { var response StartBlock _ = json.Unmarshal(line, &response) return response.ContentBlock.Text } func FormatClaudePrompt(chat_compeletion_messages []models.Message) string { var sb strings.Builder for _, message := range chat_compeletion_messages { if message.Role != "assistant" { sb.WriteString(fmt.Sprintf("\n\nHuman: %s\n\nAssistant: ", message.Content)) } else { sb.WriteString(fmt.Sprintf("%s\n", message.Content)) } } prompt := sb.String() return prompt } // response (not stream) type Response struct { ID string `json:"id"` Type string `json:"type"` Role string `json:"role"` Model string `json:"model"` Content []Content `json:"content"` StopReason string `json:"stop_reason"` StopSequence interface{} `json:"stop_sequence"` Usage Usage `json:"usage"` } type Content struct { Type string `json:"type"` Text string `json:"text"` } type Usage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` } ================================================ FILE: api/llm/gemini/gemini.go ================================================ package gemini import ( b64 "encoding/base64" "encoding/json" "fmt" "io" "log" "net/http" "os" "strings" mapset "github.com/deckarep/golang-set/v2" "github.com/samber/lo" models "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) type Part interface { toPart() string } type PartString struct { Text string `json:"text"` } func TextData(text string) PartString { return PartString{ Text: text, } } func (p *PartString) toPart() string { return p.Text } type PartBlob struct { Blob Blob `json:"inlineData"` } func (p PartBlob) toPart() string { b := p.Blob return fmt.Sprintf("data:%s;base64,%s", b.MIMEType, b.Data) } // from https://github.com/google/generative-ai-go/blob/main/genai/generativelanguagepb_veneer.gen.go#L56 // Blob contains raw media bytes. // // Text should not be sent as raw bytes, use the 'text' field. type Blob struct { // The IANA standard MIME type of the source data. // Examples: // - image/png // - image/jpeg // // If an unsupported MIME type is provided, an error will be returned. For a // complete list of supported types, see [Supported file // formats](https://ai.google.dev/gemini-api/docs/prompting_with_media#supported_file_formats). MIMEType string `json:"mimeType"` // Raw bytes for media formats. Data string `json:"data"` } func ImageData(mimeType string, data []byte) Blob { return Blob{ MIMEType: mimeType, Data: b64.StdEncoding.EncodeToString(data), } } type GeminiMessage struct { Role string `json:"role"` Parts []Part `json:"parts"` } type GeminPayload struct { Contents []GeminiMessage `json:"contents"` } type Content struct { Parts []struct { Text string `json:"text"` Thought bool `json:"thought"` } `json:"parts"` Role string `json:"role"` } type SafetyRating struct { Category string `json:"category"` Probability string `json:"probability"` } type Candidate struct { Content Content `json:"content"` FinishReason string `json:"finishReason"` Index int `json:"index"` SafetyRatings []SafetyRating `json:"safetyRatings"` } type PromptFeedback struct { SafetyRatings []SafetyRating `json:"safetyRatings"` } type ResponseBody struct { Candidates []Candidate `json:"candidates"` PromptFeedback PromptFeedback `json:"promptFeedback"` } func ParseRespLine(line []byte, answer string) string { var resp ResponseBody if err := json.Unmarshal(line, &resp); err != nil { fmt.Println("Failed to parse request body:", err) } for _, candidate := range resp.Candidates { for idx, part := range candidate.Content.Parts { if idx > 0 { answer += "\n\n" } if part.Thought { answer += ("" + part.Text + "") } else { answer += part.Text } } } return answer } // ParseRespLineDelta extracts only the delta content from a response line without accumulating func ParseRespLineDelta(line []byte) string { var resp ResponseBody if err := json.Unmarshal(line, &resp); err != nil { fmt.Println("Failed to parse request body:", err) return "" } var delta string for _, candidate := range resp.Candidates { for idx, part := range candidate.Content.Parts { if idx > 0 { delta += "\n\n" } if part.Thought { delta += ("" + part.Text + "") } else { delta += part.Text } } } return delta } func SupportedMimeTypes() mapset.Set[string] { return mapset.NewSet( "image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "video/mp4", "video/mpeg", "video/mov", "video/avi", "video/x-flv", "video/mpg", "video/webm", "video/wmv", "video/3gpp", ) } func GenGemminPayload(chat_compeletion_messages []models.Message, chatFiles []sqlc_queries.ChatFile) ([]byte, error) { payload := GeminPayload{ Contents: make([]GeminiMessage, len(chat_compeletion_messages)), } for i, message := range chat_compeletion_messages { geminiMessage := GeminiMessage{ Role: message.Role, Parts: []Part{ &PartString{Text: message.Content}, }, } if message.Role == "assistant" { geminiMessage.Role = "model" } else if message.Role == "system" { geminiMessage.Role = "user" } payload.Contents[i] = geminiMessage } if len(chatFiles) > 0 { partsFromFiles := lo.Map(chatFiles, func(chatFile sqlc_queries.ChatFile, _ int) Part { imageExt := SupportedMimeTypes() if imageExt.Contains(chatFile.MimeType) { return &PartBlob{Blob: ImageData(chatFile.MimeType, chatFile.Data)} } else { return &PartString{Text: "file: " + chatFile.Name + "\n<<<" + string(chatFile.Data) + ">>>\n"} } }) fmt.Printf("partsFromFiles: %+v\n", partsFromFiles) payload.Contents[0].Parts = append(payload.Contents[0].Parts, partsFromFiles...) } payloadBytes, err := json.Marshal(payload) log.Printf("\n%s\n", string(payloadBytes)) if err != nil { fmt.Println("Error marshalling payload:", err) // handle err return nil, err } return payloadBytes, nil } type ErrorResponse struct { Error struct { Code int `json:"code"` Message string `json:"message"` Status string `json:"status"` } `json:"error"` } func HandleRegularResponse(client http.Client, req *http.Request) (*models.LLMAnswer, error) { // Make the request resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to send Gemini API request: %w", err) } defer resp.Body.Close() // Read response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read Gemini response body: %w", err) } // Handle non-200 status codes if resp.StatusCode != http.StatusOK { var errResp ErrorResponse if jsonErr := json.Unmarshal(body, &errResp); jsonErr == nil && errResp.Error.Message != "" { return nil, fmt.Errorf("gemini API error: %s (status: %s, code: %d)", errResp.Error.Message, errResp.Error.Status, errResp.Error.Code) } return nil, fmt.Errorf("gemini API returned status %d: %s", resp.StatusCode, string(body)) } // Parse successful response var geminiResp ResponseBody if err := json.Unmarshal(body, &geminiResp); err != nil { return nil, fmt.Errorf("failed to parse Gemini response: %w", err) } // Validate response structure if len(geminiResp.Candidates) == 0 { return nil, fmt.Errorf("no candidates in Gemini response") } // Extract answer text var answer strings.Builder for _, candidate := range geminiResp.Candidates { for _, part := range candidate.Content.Parts { if part.Text != "" { if answer.Len() > 0 { answer.WriteString("\n\n") } answer.WriteString(part.Text) } } } if answer.Len() == 0 { return nil, fmt.Errorf("empty response from Gemini") } return &models.LLMAnswer{ Answer: answer.String(), AnswerId: "", // Gemini doesn't provide an ID }, nil } func BuildAPIURL(model string, stream bool) string { endpoint := "generateContent" url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:%s?key=$GEMINI_API_KEY", model, endpoint) if stream { endpoint = "streamGenerateContent?alt=sse" url = fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:%s&key=$GEMINI_API_KEY", model, endpoint) } return os.ExpandEnv(url) } type GoogleApiError struct { Error struct { Code int `json:"code"` Message string `json:"message"` Status string `json:"status"` Details string `json:"details,omitempty"` } `json:"error"` } func (gae *GoogleApiError) String() string { if gae.Error.Message == "" { return "Unknown Google API Error" } return fmt.Sprintf("Google API Error: Code=%d, Status=%s, Message=%s, Details=[%s]", gae.Error.Code, gae.Error.Status, gae.Error.Message, gae.Error.Details) } ================================================ FILE: api/llm/gemini/gemini_test.go ================================================ package gemini import ( "os" "testing" ) func TestBuildAPIURL(t *testing.T) { // Set test environment variable os.Setenv("GEMINI_API_KEY", "test-key") defer os.Unsetenv("GEMINI_API_KEY") tests := []struct { name string model string stream bool expected string }{ { name: "non-streaming request", model: "gemini-pro", stream: false, expected: "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:" + "generateContent?key=test-key", }, { name: "streaming request", model: "gemini-pro", stream: true, expected: "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:" + "streamGenerateContent?alt=sse&key=test-key", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := BuildAPIURL(tt.model, tt.stream) if got != tt.expected { t.Errorf("buildAPIURL() = %v, want %v", got, tt.expected) } }) } } ================================================ FILE: api/llm/openai/chat.go ================================================ package openai import ( "encoding/json" "errors" ) // Chat message role defined by the OpenAI API. const ( ChatMessageRoleSystem = "system" ChatMessageRoleUser = "user" ChatMessageRoleAssistant = "assistant" ChatMessageRoleFunction = "function" ChatMessageRoleTool = "tool" ) const chatCompletionsSuffix = "/chat/completions" var ( ErrChatCompletionInvalidModel = errors.New("this model is not supported with this method, please use CreateCompletion client method instead") //nolint:lll ErrChatCompletionStreamNotSupported = errors.New("streaming is not supported with this method, please use CreateChatCompletionStream") //nolint:lll ErrContentFieldsMisused = errors.New("can't use both Content and MultiContent properties simultaneously") ) type Hate struct { Filtered bool `json:"filtered"` Severity string `json:"severity,omitempty"` } type SelfHarm struct { Filtered bool `json:"filtered"` Severity string `json:"severity,omitempty"` } type Sexual struct { Filtered bool `json:"filtered"` Severity string `json:"severity,omitempty"` } type Violence struct { Filtered bool `json:"filtered"` Severity string `json:"severity,omitempty"` } type JailBreak struct { Filtered bool `json:"filtered"` Detected bool `json:"detected"` } type Profanity struct { Filtered bool `json:"filtered"` Detected bool `json:"detected"` } type ContentFilterResults struct { Hate Hate `json:"hate,omitempty"` SelfHarm SelfHarm `json:"self_harm,omitempty"` Sexual Sexual `json:"sexual,omitempty"` Violence Violence `json:"violence,omitempty"` JailBreak JailBreak `json:"jailbreak,omitempty"` Profanity Profanity `json:"profanity,omitempty"` } type PromptAnnotation struct { PromptIndex int `json:"prompt_index,omitempty"` ContentFilterResults ContentFilterResults `json:"content_filter_results,omitempty"` } type ImageURLDetail string const ( ImageURLDetailHigh ImageURLDetail = "high" ImageURLDetailLow ImageURLDetail = "low" ImageURLDetailAuto ImageURLDetail = "auto" ) type ChatMessageImageURL struct { URL string `json:"url,omitempty"` Detail ImageURLDetail `json:"detail,omitempty"` } type ChatMessagePartType string const ( ChatMessagePartTypeText ChatMessagePartType = "text" ChatMessagePartTypeImageURL ChatMessagePartType = "image_url" ) type ChatMessagePart struct { Type ChatMessagePartType `json:"type,omitempty"` Text string `json:"text,omitempty"` ImageURL *ChatMessageImageURL `json:"image_url,omitempty"` } type ChatCompletionMessage struct { Role string `json:"role"` Content string `json:"content"` Refusal string `json:"refusal,omitempty"` MultiContent []ChatMessagePart // This property isn't in the official documentation, but it's in // the documentation for the official library for python: // - https://github.com/openai/openai-python/blob/main/chatml.md // - https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb Name string `json:"name,omitempty"` FunctionCall *FunctionCall `json:"function_call,omitempty"` // For Role=assistant prompts this may be set to the tool calls generated by the model, such as function calls. ToolCalls []ToolCall `json:"tool_calls,omitempty"` // For Role=tool prompts this should be set to the ID given in the assistant's prior request to call a tool. ToolCallID string `json:"tool_call_id,omitempty"` } func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) { if m.Content != "" && m.MultiContent != nil { return nil, ErrContentFieldsMisused } if len(m.MultiContent) > 0 { msg := struct { Role string `json:"role"` Content string `json:"-"` Refusal string `json:"refusal,omitempty"` MultiContent []ChatMessagePart `json:"content,omitempty"` Name string `json:"name,omitempty"` FunctionCall *FunctionCall `json:"function_call,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` }(m) return json.Marshal(msg) } msg := struct { Role string `json:"role"` Content string `json:"content"` Refusal string `json:"refusal,omitempty"` MultiContent []ChatMessagePart `json:"-"` Name string `json:"name,omitempty"` FunctionCall *FunctionCall `json:"function_call,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` }(m) return json.Marshal(msg) } func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error { msg := struct { Role string `json:"role"` Content string `json:"content"` Refusal string `json:"refusal,omitempty"` MultiContent []ChatMessagePart Name string `json:"name,omitempty"` FunctionCall *FunctionCall `json:"function_call,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` }{} if err := json.Unmarshal(bs, &msg); err == nil { *m = ChatCompletionMessage(msg) return nil } multiMsg := struct { Role string `json:"role"` Content string Refusal string `json:"refusal,omitempty"` MultiContent []ChatMessagePart `json:"content"` Name string `json:"name,omitempty"` FunctionCall *FunctionCall `json:"function_call,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` }{} if err := json.Unmarshal(bs, &multiMsg); err != nil { return err } *m = ChatCompletionMessage(multiMsg) return nil } type ToolCall struct { // Index is not nil only in chat completion chunk object Index *int `json:"index,omitempty"` ID string `json:"id,omitempty"` Type ToolType `json:"type"` Function FunctionCall `json:"function"` } type FunctionCall struct { Name string `json:"name,omitempty"` // call function with arguments in JSON format Arguments string `json:"arguments,omitempty"` } type ChatCompletionResponseFormatType string const ( ChatCompletionResponseFormatTypeJSONObject ChatCompletionResponseFormatType = "json_object" ChatCompletionResponseFormatTypeJSONSchema ChatCompletionResponseFormatType = "json_schema" ChatCompletionResponseFormatTypeText ChatCompletionResponseFormatType = "text" ) type ChatCompletionResponseFormat struct { Type ChatCompletionResponseFormatType `json:"type,omitempty"` JSONSchema *ChatCompletionResponseFormatJSONSchema `json:"json_schema,omitempty"` } type ChatCompletionResponseFormatJSONSchema struct { Name string `json:"name"` Description string `json:"description,omitempty"` Schema json.Marshaler `json:"schema"` Strict bool `json:"strict"` } // ChatCompletionRequest represents a request structure for chat completion API. type ChatCompletionRequest struct { Model string `json:"model"` Messages []ChatCompletionMessage `json:"messages"` // MaxTokens The maximum number of tokens that can be generated in the chat completion. // This value can be used to control costs for text generated via API. // This value is now deprecated in favor of max_completion_tokens, and is not compatible with o1 series models. // refs: https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_tokens MaxTokens int `json:"max_tokens,omitempty"` // MaxCompletionTokens An upper bound for the number of tokens that can be generated for a completion, // including visible output tokens and reasoning tokens https://platform.openai.com/docs/guides/reasoning MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` Temperature float32 `json:"temperature,omitempty"` TopP float32 `json:"top_p,omitempty"` N int `json:"n,omitempty"` Stream bool `json:"stream,omitempty"` Stop []string `json:"stop,omitempty"` PresencePenalty float32 `json:"presence_penalty,omitempty"` ResponseFormat *ChatCompletionResponseFormat `json:"response_format,omitempty"` Seed *int `json:"seed,omitempty"` FrequencyPenalty float32 `json:"frequency_penalty,omitempty"` // LogitBias is must be a token id string (specified by their token ID in the tokenizer), not a word string. // incorrect: `"logit_bias":{"You": 6}`, correct: `"logit_bias":{"1639": 6}` // refs: https://platform.openai.com/docs/api-reference/chat/create#chat/create-logit_bias LogitBias map[string]int `json:"logit_bias,omitempty"` // LogProbs indicates whether to return log probabilities of the output tokens or not. // If true, returns the log probabilities of each output token returned in the content of message. // This option is currently not available on the gpt-4-vision-preview model. LogProbs bool `json:"logprobs,omitempty"` // TopLogProbs is an integer between 0 and 5 specifying the number of most likely tokens to return at each // token position, each with an associated log probability. // logprobs must be set to true if this parameter is used. TopLogProbs int `json:"top_logprobs,omitempty"` User string `json:"user,omitempty"` // Deprecated: use Tools instead. Functions []FunctionDefinition `json:"functions,omitempty"` // Deprecated: use ToolChoice instead. FunctionCall any `json:"function_call,omitempty"` Tools []Tool `json:"tools,omitempty"` // This can be either a string or an ToolChoice object. ToolChoice any `json:"tool_choice,omitempty"` // Options for streaming response. Only set this when you set stream: true. StreamOptions *StreamOptions `json:"stream_options,omitempty"` // Disable the default behavior of parallel tool calls by setting it: false. ParallelToolCalls any `json:"parallel_tool_calls,omitempty"` // Store can be set to true to store the output of this completion request for use in distillations and evals. // https://platform.openai.com/docs/api-reference/chat/create#chat-create-store Store bool `json:"store,omitempty"` // Metadata to store with the completion. Metadata map[string]string `json:"metadata,omitempty"` } type StreamOptions struct { // If set, an additional chunk will be streamed before the data: [DONE] message. // The usage field on this chunk shows the token usage statistics for the entire request, // and the choices field will always be an empty array. // All other chunks will also include a usage field, but with a null value. IncludeUsage bool `json:"include_usage,omitempty"` } type ToolType string const ( ToolTypeFunction ToolType = "function" ) type Tool struct { Type ToolType `json:"type"` Function *FunctionDefinition `json:"function,omitempty"` } type ToolChoice struct { Type ToolType `json:"type"` Function ToolFunction `json:"function,omitempty"` } type ToolFunction struct { Name string `json:"name"` } type FunctionDefinition struct { Name string `json:"name"` Description string `json:"description,omitempty"` Strict bool `json:"strict,omitempty"` // Parameters is an object describing the function. // You can pass json.RawMessage to describe the schema, // or you can pass in a struct which serializes to the proper JSON schema. // The jsonschema package is provided for convenience, but you should // consider another specialized library if you require more complex schemas. Parameters any `json:"parameters"` } // Deprecated: use FunctionDefinition instead. type FunctionDefine = FunctionDefinition type TopLogProbs struct { Token string `json:"token"` LogProb float64 `json:"logprob"` Bytes []byte `json:"bytes,omitempty"` } // LogProb represents the probability information for a token. type LogProb struct { Token string `json:"token"` LogProb float64 `json:"logprob"` Bytes []byte `json:"bytes,omitempty"` // Omitting the field if it is null // TopLogProbs is a list of the most likely tokens and their log probability, at this token position. // In rare cases, there may be fewer than the number of requested top_logprobs returned. TopLogProbs []TopLogProbs `json:"top_logprobs"` } // LogProbs is the top-level structure containing the log probability information. type LogProbs struct { // Content is a list of message content tokens with log probability information. Content []LogProb `json:"content"` } type FinishReason string const ( FinishReasonStop FinishReason = "stop" FinishReasonLength FinishReason = "length" FinishReasonFunctionCall FinishReason = "function_call" FinishReasonToolCalls FinishReason = "tool_calls" FinishReasonContentFilter FinishReason = "content_filter" FinishReasonNull FinishReason = "null" ) func (r FinishReason) MarshalJSON() ([]byte, error) { if r == FinishReasonNull || r == "" { return []byte("null"), nil } return []byte(`"` + string(r) + `"`), nil // best effort to not break future API changes } type ChatCompletionChoice struct { Index int `json:"index"` Message ChatCompletionMessage `json:"message"` // FinishReason // stop: API returned complete message, // or a message terminated by one of the stop sequences provided via the stop parameter // length: Incomplete model output due to max_tokens parameter or token limit // function_call: The model decided to call a function // content_filter: Omitted content due to a flag from our content filters // null: API response still in progress or incomplete FinishReason FinishReason `json:"finish_reason"` LogProbs *LogProbs `json:"logprobs,omitempty"` ContentFilterResults ContentFilterResults `json:"content_filter_results,omitempty"` } // ChatCompletionResponse represents a response structure for chat completion API. type ChatCompletionResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []ChatCompletionChoice `json:"choices"` Usage Usage `json:"usage"` SystemFingerprint string `json:"system_fingerprint"` PromptFilterResults []PromptFilterResult `json:"prompt_filter_results,omitempty"` httpHeader } ================================================ FILE: api/llm/openai/client.go ================================================ package openai import "net/http" type httpHeader http.Header func (h *httpHeader) SetHeader(header http.Header) { *h = httpHeader(header) } func (h *httpHeader) Header() http.Header { return http.Header(*h) } ================================================ FILE: api/llm/openai/common.go ================================================ package openai // common.go defines common types used throughout the OpenAI API. // Usage Represents the total token usage per request to OpenAI. type Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` PromptTokensDetails *PromptTokensDetails `json:"prompt_tokens_details"` CompletionTokensDetails *CompletionTokensDetails `json:"completion_tokens_details"` } // CompletionTokensDetails Breakdown of tokens used in a completion. type CompletionTokensDetails struct { AudioTokens int `json:"audio_tokens"` ReasoningTokens int `json:"reasoning_tokens"` } // PromptTokensDetails Breakdown of tokens used in the prompt. type PromptTokensDetails struct { AudioTokens int `json:"audio_tokens"` CachedTokens int `json:"cached_tokens"` } ================================================ FILE: api/llm/openai/openai.go ================================================ package openai // code is copied from openai go to add reasoningContent field type ChatCompletionStreamChoiceDelta struct { Content string `json:"content,omitempty"` ReasoningContent string `json:"reasoning_content,omitempty"` Role string `json:"role,omitempty"` FunctionCall *FunctionCall `json:"function_call,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` Refusal string `json:"refusal,omitempty"` } type ChatCompletionStreamChoiceLogprobs struct { Content []ChatCompletionTokenLogprob `json:"content,omitempty"` Refusal []ChatCompletionTokenLogprob `json:"refusal,omitempty"` } type ChatCompletionTokenLogprob struct { Token string `json:"token"` Bytes []int64 `json:"bytes,omitempty"` Logprob float64 `json:"logprob,omitempty"` TopLogprobs []ChatCompletionTokenLogprobTopLogprob `json:"top_logprobs"` } type ChatCompletionTokenLogprobTopLogprob struct { Token string `json:"token"` Bytes []int64 `json:"bytes"` Logprob float64 `json:"logprob"` } type ChatCompletionStreamChoice struct { Index int `json:"index"` Delta ChatCompletionStreamChoiceDelta `json:"delta"` Logprobs *ChatCompletionStreamChoiceLogprobs `json:"logprobs,omitempty"` FinishReason FinishReason `json:"finish_reason"` ContentFilterResults ContentFilterResults `json:"content_filter_results,omitempty"` } type PromptFilterResult struct { Index int `json:"index"` ContentFilterResults ContentFilterResults `json:"content_filter_results,omitempty"` } type ChatCompletionStreamResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []ChatCompletionStreamChoice `json:"choices"` SystemFingerprint string `json:"system_fingerprint"` PromptAnnotations []PromptAnnotation `json:"prompt_annotations,omitempty"` PromptFilterResults []PromptFilterResult `json:"prompt_filter_results,omitempty"` // An optional field that will only be present when you set stream_options: {"include_usage": true} in your request. // When present, it contains a null value except for the last chunk which contains the token usage statistics // for the entire request. Usage *Usage `json:"usage,omitempty"` } ================================================ FILE: api/llm_openai.go ================================================ package main import ( "encoding/base64" "fmt" "log" "net/http" "net/url" "os" "strings" "time" mapset "github.com/deckarep/golang-set/v2" "github.com/samber/lo" openai "github.com/sashabaranov/go-openai" models "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) func SupportedMimeTypes() mapset.Set[string] { return mapset.NewSet( "image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "video/mp4", "video/mpeg", "video/mov", "video/avi", "video/x-flv", "video/mpg", "video/webm", "video/wmv", "video/3gpp", ) } func messagesToOpenAIMesages(messages []models.Message, chatFiles []sqlc_queries.ChatFile) []openai.ChatCompletionMessage { open_ai_msgs := lo.Map(messages, func(m models.Message, _ int) openai.ChatCompletionMessage { return openai.ChatCompletionMessage{Role: m.Role, Content: m.Content} }) if len(chatFiles) == 0 { return open_ai_msgs } parts := lo.Map(chatFiles, func(m sqlc_queries.ChatFile, _ int) openai.ChatMessagePart { if SupportedMimeTypes().Contains(m.MimeType) { return openai.ChatMessagePart{ Type: openai.ChatMessagePartTypeImageURL, ImageURL: &openai.ChatMessageImageURL{ URL: byteToImageURL(m.MimeType, m.Data), Detail: openai.ImageURLDetailAuto, }, } } else { return openai.ChatMessagePart{ Type: openai.ChatMessagePartTypeText, Text: "file: " + m.Name + "\n<<<" + string(m.Data) + ">>>\n", } } }) // first user message firstUserMessage, idx, found := lo.FindIndexOf(open_ai_msgs, func(msg openai.ChatCompletionMessage) bool { return msg.Role == "user" }) if found { log.Printf("firstUserMessage: %+v\n", firstUserMessage) open_ai_msgs[idx].MultiContent = append( []openai.ChatMessagePart{ {Type: openai.ChatMessagePartTypeText, Text: firstUserMessage.Content}, }, parts...) open_ai_msgs[idx].Content = "" log.Printf("firstUserMessage: %+v\n", firstUserMessage) } return open_ai_msgs } func byteToImageURL(mimeType string, data []byte) string { b64 := fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(data)) return b64 } func getModelBaseUrl(apiUrl string) (string, error) { if apiUrl == "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" { return "https://dashscope.aliyuncs.com/compatible-mode/v1", nil } // open router // https://openrouter.ai/api/v1 if strings.Contains(apiUrl, "openrouter") { // keep the url until /v1 slashIndex := strings.Index(apiUrl, "/v1") if slashIndex > 0 { return apiUrl[:slashIndex] + "/v1", nil } return apiUrl, nil } parsedUrl, err := url.Parse(apiUrl) if err != nil { return "", err } slashIndex := strings.Index(parsedUrl.Path[1:], "/") version := "" // https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions if slashIndex > 0 { version = parsedUrl.Path[1 : slashIndex+1] } return fmt.Sprintf("%s://%s/%s", parsedUrl.Scheme, parsedUrl.Host, version), nil } func configOpenAIProxy(config *openai.ClientConfig) { proxyUrlStr := appConfig.OPENAI.PROXY_URL if proxyUrlStr != "" { proxyUrl, err := url.Parse(proxyUrlStr) if err != nil { log.Printf("Error parsing proxy URL: %v", err) } transport := &http.Transport{ Proxy: http.ProxyURL(proxyUrl), } config.HTTPClient = &http.Client{ Transport: transport, Timeout: 120 * time.Second, } } } func genOpenAIConfig(chatModel sqlc_queries.ChatModel) (openai.ClientConfig, error) { token := os.Getenv(chatModel.ApiAuthKey) baseUrl, err := getModelBaseUrl(chatModel.Url) log.Printf("baseUrl: %s\n", baseUrl) if err != nil { return openai.ClientConfig{}, err } var config openai.ClientConfig if os.Getenv("AZURE_RESOURCE_NAME") != "" { config = openai.DefaultAzureConfig(token, chatModel.Url) config.AzureModelMapperFunc = func(model string) string { azureModelMapping := map[string]string{ "gpt-3.5-turbo": os.Getenv("AZURE_RESOURCE_NAME"), } return azureModelMapping[model] } } else { config = openai.DefaultConfig(token) config.BaseURL = baseUrl // two minutes timeout // config.HTTPClient.Timeout = 120 * time.Second configOpenAIProxy(&config) } return config, err } ================================================ FILE: api/llm_summary.go ================================================ package main import ( "context" "log" "strings" "time" "github.com/tmc/langchaingo/chains" "github.com/tmc/langchaingo/documentloaders" "github.com/tmc/langchaingo/llms/openai" "github.com/tmc/langchaingo/textsplitter" ) func llm_summarize_with_timeout(baseURL, content string) string { // Create a context with a 20 second timeout ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() // Call the summarize function with the context summary := llm_summarize(ctx, baseURL, content) return summary } func llm_summarize(ctx context.Context, baseURL string, doc string) string { baseURL = strings.TrimSuffix(baseURL, "/v1") llm, err := openai.New( openai.WithToken(appConfig.OPENAI.API_KEY), openai.WithBaseURL(baseURL), ) if err != nil { log.Printf("failed to create openai client %s: %v", baseURL, err) return "" } llmSummarizationChain := chains.LoadRefineSummarization(llm) docs, _ := documentloaders.NewText(strings.NewReader(doc)).LoadAndSplit(ctx, textsplitter.NewRecursiveCharacter(), ) outputValues, err := chains.Call(ctx, llmSummarizationChain, map[string]any{"input_documents": docs}) if err != nil { log.Printf("failed to call chain: %s, %v", baseURL, err) return "" } out := outputValues["text"].(string) return out } ================================================ FILE: api/main.go ================================================ package main import ( "context" "database/sql" _ "embed" "fmt" "net/http" "os" "path/filepath" "reflect" "strings" "time" "github.com/gorilla/handlers" "github.com/gorilla/mux" _ "github.com/lib/pq" log "github.com/sirupsen/logrus" "github.com/spf13/viper" "github.com/swuecho/chat_backend/sqlc_queries" "github.com/swuecho/chat_backend/static" "golang.org/x/time/rate" ) var logger *log.Logger type AppConfig struct { OPENAI struct { API_KEY string RATELIMIT int PROXY_URL string } CLAUDE struct { API_KEY string } PG struct { HOST string PORT int USER string PASS string DB string } } var appConfig AppConfig var jwtSecretAndAud sqlc_queries.JwtSecret func getFlattenKeys(prefix string, v reflect.Value) (keys []string) { switch v.Kind() { case reflect.Struct: for i := 0; i < v.NumField(); i++ { field := v.Field(i) name := v.Type().Field(i).Name keys = append(keys, getFlattenKeys(prefix+name+".", field)...) } default: keys = append(keys, prefix[:len(prefix)-1]) } return keys } func bindEnvironmentVariables() { appConfig = AppConfig{} for _, key := range getFlattenKeys("", reflect.ValueOf(appConfig)) { envKey := strings.ToUpper(strings.ReplaceAll(key, ".", "_")) err := viper.BindEnv(key, envKey) if err != nil { logger.Fatal("config: unable to bind env: " + err.Error()) } } } //go:embed sqlc/schema.sql var schemaBytes []byte // lastRequest tracks the last time a request was received var lastRequest time.Time var openAIRateLimiter *rate.Limiter func main() { // Allow only 3000 requests per minute, with burst 500 openAIRateLimiter = rate.NewLimiter(rate.Every(time.Minute/3000), 500) // A buffered channel with capacity 1 // This ensures only one API call can proceed at a time lastRequest = time.Now() // Configure viper to read environment variables bindEnvironmentVariables() viper.AutomaticEnv() if err := viper.Unmarshal(&appConfig); err != nil { logger.Fatal("config: unable to decode into struct: " + err.Error()) } log.Printf("%+v", appConfig) logger = log.New() logger.Formatter = &log.JSONFormatter{} // Establish a database connection dbURL := os.Getenv("DATABASE_URL") var connStr string if dbURL == "" { pg := appConfig.PG connStr = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", pg.HOST, pg.PORT, pg.USER, pg.PASS, pg.DB) print(connStr) } else { connStr = dbURL } pgdb, err := sql.Open("postgres", connStr) if err != nil { log.Fatal(err) } defer pgdb.Close() // Get current executable file path ex, err := os.Executable() if err != nil { log.WithError(err).Fatal("Failed to get executable path") } // Get current project directory projectDir := filepath.Dir(ex) // Print project directory fmt.Println(projectDir) sqlStatements := string(schemaBytes) // Execute SQL statements _, err = pgdb.Exec(sqlStatements) if err != nil { log.WithError(err).Fatal("Failed to execute SQL schema statements") } fmt.Println("SQL statements executed successfully") // create a new Gorilla Mux router instance // Create a new router router := mux.NewRouter() apiRouter := router.PathPrefix("/api").Subrouter() sqlc_q := sqlc_queries.New(pgdb) secretService := NewJWTSecretService(sqlc_q) jwtSecretAndAud, err = secretService.GetOrCreateJwtSecret(context.Background(), "chat") if err != nil { log.Fatal(err) } // Create separate subrouters for admin and user routes adminRouter := apiRouter.PathPrefix("/admin").Subrouter() userRouter := apiRouter.NewRoute().Subrouter() // Apply different middleware to admin and user routes adminRouter.Use(AdminAuthMiddleware) userRouter.Use(UserAuthMiddleware) ChatModelHandler := NewChatModelHandler(sqlc_q) ChatModelHandler.Register(userRouter) // Chat models for regular users // create a new AuthUserHandler instance for user routes userHandler := NewAuthUserHandler(sqlc_q) // register authenticated routes with the user router userHandler.Register(userRouter) // register public routes (login/signup) with the api router (no auth required) userHandler.RegisterPublicRoutes(apiRouter) // create a new AdminHandler instance for admin-only routes authUserService := NewAuthUserService(sqlc_q) adminHandler := NewAdminHandler(authUserService) // register the AdminHandler with the admin router (will remove /admin prefix automatically) adminHandler.RegisterRoutes(adminRouter) promptHandler := NewChatPromptHandler(sqlc_q) promptHandler.Register(userRouter) chatSessionHandler := NewChatSessionHandler(sqlc_q) chatSessionHandler.Register(userRouter) // Register active session handler before workspace handler to avoid route shadowing activeSessionHandler := NewUserActiveChatSessionHandler(sqlc_q) activeSessionHandler.Register(userRouter) chatWorkspaceHandler := NewChatWorkspaceHandler(sqlc_q) chatWorkspaceHandler.Register(userRouter) chatMessageHandler := NewChatMessageHandler(sqlc_q) chatMessageHandler.Register(userRouter) chatSnapshotHandler := NewChatSnapshotHandler(sqlc_q) chatSnapshotHandler.Register(userRouter) // create a new ChatHandler instance chatHandler := NewChatHandler(sqlc_q) chatHandler.Register(userRouter) user_model_privilege_handler := NewUserChatModelPrivilegeHandler(sqlc_q) user_model_privilege_handler.Register(userRouter) chatFileHandler := NewChatFileHandler(sqlc_q) chatFileHandler.Register(userRouter) chatCommentHandler := NewChatCommentHandler(sqlc_q) chatCommentHandler.Register(userRouter) botAnswerHistoryHandler := NewBotAnswerHistoryHandler(sqlc_q) botAnswerHistoryHandler.Register(userRouter) apiRouter.HandleFunc("/tts", handleTTSRequest) apiRouter.HandleFunc("/errors", ErrorCatalogHandler) // Embed static/* directory fs := http.FileServer(http.FS(static.StaticFiles)) // Set cache headers for static/assets files cacheHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/static/") { w.Header().Set("Cache-Control", "max-age=31536000") // 1 year } else if r.URL.Path == "" { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") } fs.ServeHTTP(w, r) }) router.PathPrefix("/").Handler(makeGzipHandler(cacheHandler)) // fly.io if os.Getenv("FLY_APP_NAME") != "" { router.Use(UpdateLastRequestTime) } // Apply rate limiting to authenticated routes only limitedRouter := RateLimitByUserID(sqlc_q) adminRouter.Use(limitedRouter) userRouter.Use(limitedRouter) // Public routes (apiRouter) don't need authentication or rate limiting // TTS and errors endpoints are public // Add CORS middleware to handle cross-origin requests defaultOrigins := []string{"http://localhost:9002", "http://localhost:3000"} allowedOrigins := append([]string{}, defaultOrigins...) restrictToConfigured := false if corsOrigins := os.Getenv("CORS_ALLOWED_ORIGINS"); corsOrigins != "" { parts := strings.Split(corsOrigins, ",") allowedOrigins = allowedOrigins[:0] for _, origin := range parts { trimmed := strings.TrimSpace(origin) if trimmed == "" { continue } allowedOrigins = append(allowedOrigins, trimmed) } restrictToConfigured = true } originValidator := func(origin string) bool { if len(allowedOrigins) == 0 { return true } for _, allowed := range allowedOrigins { if allowed == "*" { return true } if strings.EqualFold(origin, allowed) { return true } } if !restrictToConfigured { if strings.HasPrefix(origin, "http://localhost:") || strings.HasPrefix(origin, "http://127.0.0.1:") { return true } if strings.HasPrefix(origin, "https://localhost:") || strings.HasPrefix(origin, "https://127.0.0.1:") { return true } } return false } corsOptions := handlers.CORS( handlers.AllowedOriginValidator(originValidator), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}), handlers.AllowedHeaders([]string{"Content-Type", "Authorization", "Cache-Control", "Connection", "Pragma", "Accept", "Accept-Language", "Origin", "Referer"}), handlers.AllowCredentials(), ) // Wrap the router with CORS and logging middleware corsRouter := corsOptions(router) loggedRouter := handlers.LoggingHandler(logger.Out, corsRouter) router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { tpl, err1 := route.GetPathTemplate() met, err2 := route.GetMethods() fmt.Println(tpl, err1, met, err2) return nil }) // fly.io if os.Getenv("FLY_APP_NAME") != "" { // read env var FLY_RESTART_INTERVAL_IF_IDLE if not set, set to 30 minutes restartInterval := os.Getenv("FLY_RESTART_INTERVAL_IF_IDLE") // If not set, default to 30 minutes if restartInterval == "" { restartInterval = "30m" } duration, err := time.ParseDuration(restartInterval) if err != nil { log.Println("Invalid FLY_RESTART_INTERVAL_IF_IDLE value. Exiting.") } // Use a goroutine to check for inactivity and exit go func() { for { time.Sleep(1 * time.Minute) // Check every minute if time.Since(lastRequest) > duration { fmt.Printf("No activity for %s. Exiting.", restartInterval) os.Exit(0) return } } }() } err = http.ListenAndServe(":8080", loggedRouter) if err != nil { log.Fatal(err) } } ================================================ FILE: api/main_test.go ================================================ package main import ( "database/sql" "fmt" "io" "os" "testing" "time" _ "github.com/lib/pq" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" log "github.com/sirupsen/logrus" ) var db *sql.DB func TestMain(m *testing.M) { // uses a sensible default on windows (tcp/http) and linux/osx (socket) pool, err := dockertest.NewPool("") if err != nil { log.Fatalf("Could not construct pool: %s", err) } err = pool.Client.Ping() if err != nil { log.Fatalf("Could not connect to Docker: %s", err) } // pulls an image, creates a container based on it and runs it resource, err := pool.RunWithOptions(&dockertest.RunOptions{ Repository: "postgres", Tag: "12", Env: []string{ "POSTGRES_PASSWORD=secret", "POSTGRES_USER=user_name", "POSTGRES_DB=dbname", "listen_addresses = '*'", }, }, func(config *docker.HostConfig) { // set AutoRemove to true so that stopped container goes away by itself config.AutoRemove = true config.RestartPolicy = docker.RestartPolicy{Name: "no"} }) if err != nil { log.Fatalf("Could not start resource: %s", err) } hostAndPort := resource.GetHostPort("5432/tcp") databaseUrl := fmt.Sprintf("postgres://user_name:secret@%s/dbname?sslmode=disable", hostAndPort) log.Println("Connecting to database on url: ", databaseUrl) resource.Expire(120) // Tell docker to hard kill the container in 120 seconds // exponential backoff-retry, because the application in the container might not be ready to accept connections yet pool.MaxWait = 120 * time.Second if err = pool.Retry(func() error { db, err = sql.Open("postgres", databaseUrl) if err != nil { return err } return db.Ping() }); err != nil { log.Fatalf("Could not connect to docker: %s", err) } //////// init schema file, err := os.Open("./sqlc/schema.sql") if err != nil { log.Fatalf("Could not open file: %s", err) } defer file.Close() bytes, err := io.ReadAll(file) if err != nil { log.Fatalf("Could not read file: %s", err) } _, err = db.Exec(string(bytes)) if err != nil { log.Fatalf("Could not execute SQL: %s", err) } //Run tests code := m.Run() // You can't defer this because os.Exit doesn't care for defer if err := pool.Purge(resource); err != nil { log.Fatalf("Could not purge resource: %s", err) } os.Exit(code) } ================================================ FILE: api/middleware_authenticate.go ================================================ package main import ( "context" "fmt" "net/http" "strings" jwt "github.com/golang-jwt/jwt/v5" "github.com/swuecho/chat_backend/auth" ) func CheckPermission(userID int, ctx context.Context) bool { contextUserID, ok := ctx.Value("user_id").(int) if !ok { return false } role, ok := ctx.Value("role").(string) if !ok { return false } switch role { case "admin": return true case "member": return userID == contextUserID default: return false } } type AuthTokenResult struct { Token *jwt.Token Claims jwt.MapClaims UserID string Role string TokenType string Valid bool Error *APIError } func extractBearerToken(r *http.Request) string { // Extract from Authorization header for access tokens bearerToken := r.Header.Get("Authorization") tokenParts := strings.Split(bearerToken, " ") if len(tokenParts) == 2 { return tokenParts[1] } return "" } func createUserContext(r *http.Request, userID, role string) *http.Request { ctx := context.WithValue(r.Context(), userContextKey, userID) ctx = context.WithValue(ctx, roleContextKey, role) return r.WithContext(ctx) } func parseAndValidateJWT(bearerToken string, expectedTokenType string) *AuthTokenResult { result := &AuthTokenResult{} if bearerToken == "" { err := ErrAuthInvalidCredentials err.Detail = "Authorization token required" result.Error = &err return result } jwtSigningKey := []byte(jwtSecretAndAud.Secret) token, err := jwt.Parse(bearerToken, func(token *jwt.Token) (any, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("invalid JWT signing method") } return jwtSigningKey, nil }) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.Detail = "Invalid authorization token" result.Error = &apiErr return result } if !token.Valid { apiErr := ErrAuthInvalidCredentials apiErr.Detail = "Token is not valid" result.Error = &apiErr return result } claims, ok := token.Claims.(jwt.MapClaims) if !ok { apiErr := ErrAuthInvalidCredentials apiErr.Detail = "Cannot parse token claims" result.Error = &apiErr return result } userID, ok := claims["user_id"].(string) if !ok { apiErr := ErrAuthInvalidCredentials apiErr.Detail = "User ID not found in token" result.Error = &apiErr return result } role, ok := claims["role"].(string) if !ok { apiErr := ErrAuthInvalidCredentials apiErr.Detail = "User role not found in token" result.Error = &apiErr return result } tokenType, ok := claims["token_type"].(string) if !ok { // Legacy forever tokens were generated before the token_type claim existed. // Treat them as access tokens so they remain usable. if expectedTokenType == "" || expectedTokenType == auth.TokenTypeAccess { tokenType = auth.TokenTypeAccess } else { apiErr := ErrAuthInvalidCredentials apiErr.Detail = "Token type not found in token" result.Error = &apiErr return result } } if expectedTokenType != "" && tokenType != expectedTokenType { apiErr := ErrAuthInvalidCredentials apiErr.Detail = "Token type is not valid for this operation" result.Error = &apiErr return result } result.Token = token result.Claims = claims result.UserID = userID result.Role = role result.TokenType = tokenType result.Valid = true return result } type contextKey string const ( roleContextKey contextKey = "role" userContextKey contextKey = "user" guidContextKey contextKey = "guid" ) const snapshotPrefix = "/api/uuid/chat_snapshot/" func IsChatSnapshotUUID(r *http.Request) bool { // Check http method is GET if r.Method != http.MethodGet { return false } // Check if request url path has the required prefix and does not have "/all" suffix if strings.HasPrefix(r.URL.Path, snapshotPrefix) && !strings.HasSuffix(r.URL.Path, "/all") { return true } return false } func AdminOnlyHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userRole, ok := ctx.Value(roleContextKey).(string) if !ok { apiErr := ErrAuthAdminRequired apiErr.Detail = "User role information not found" RespondWithAPIError(w, apiErr) return } if userRole != "admin" { apiErr := ErrAuthAdminRequired apiErr.Detail = "Current user does not have admin role" RespondWithAPIError(w, apiErr) return } h.ServeHTTP(w, r) }) } func AdminOnlyHandlerFunc(handlerFunc http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userRole, ok := ctx.Value(roleContextKey).(string) if !ok { apiErr := ErrAuthAdminRequired apiErr.Detail = "User role information not found" RespondWithAPIError(w, apiErr) return } if userRole != "admin" { apiErr := ErrAuthAdminRequired apiErr.Detail = "Current user does not have admin role" RespondWithAPIError(w, apiErr) return } handlerFunc(w, r) } } // AdminRouteMiddleware applies admin-only protection to all routes in a subrouter func AdminRouteMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userRole, ok := ctx.Value(roleContextKey).(string) if !ok { apiErr := ErrAuthAdminRequired apiErr.Detail = "User role information not found" RespondWithAPIError(w, apiErr) return } if userRole != "admin" { apiErr := ErrAuthAdminRequired apiErr.Detail = "Admin privileges required for this endpoint" RespondWithAPIError(w, apiErr) return } next.ServeHTTP(w, r) }) } // AdminAuthMiddleware - Authentication middleware specifically for admin routes func AdminAuthMiddleware(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { bearerToken := extractBearerToken(r) result := parseAndValidateJWT(bearerToken, auth.TokenTypeAccess) if result.Error != nil { RespondWithAPIError(w, *result.Error) return } // Admin-only check if result.Role != "admin" { apiErr := ErrAuthAdminRequired apiErr.Detail = "Admin privileges required" RespondWithAPIError(w, apiErr) return } // Add user context and proceed handler.ServeHTTP(w, createUserContext(r, result.UserID, result.Role)) }) } // UserAuthMiddleware - Authentication middleware for regular user routes func UserAuthMiddleware(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { bearerToken := extractBearerToken(r) result := parseAndValidateJWT(bearerToken, auth.TokenTypeAccess) if result.Error != nil { RespondWithAPIError(w, *result.Error) return } // Add user context and proceed (no role restrictions for user middleware) handler.ServeHTTP(w, createUserContext(r, result.UserID, result.Role)) }) } func IsAuthorizedMiddleware(handler http.Handler) http.Handler { noAuthPaths := map[string]bool{ "/": true, "/favicon.ico": true, "/api/login": true, "/api/signup": true, "/api/tts": true, "/api/errors": true, } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, ok := noAuthPaths[r.URL.Path]; ok || strings.HasPrefix(r.URL.Path, "/static") || IsChatSnapshotUUID(r) { handler.ServeHTTP(w, r) return } bearerToken := extractBearerToken(r) result := parseAndValidateJWT(bearerToken, auth.TokenTypeAccess) if result.Error != nil { RespondWithAPIError(w, *result.Error) return } if result.Valid { // superuser if strings.HasPrefix(r.URL.Path, "/admin") && result.Role != "admin" { apiErr := ErrAuthAdminRequired apiErr.Detail = "This endpoint requires admin privileges" RespondWithAPIError(w, apiErr) return } // TODO: get trace id and add it to context //traceID := r.Header.Get("X-Request-Id") //if len(traceID) > 0 { //ctx = context.WithValue(ctx, guidContextKey, traceID) //} // Store user ID and role in the request context // pass token to request handler.ServeHTTP(w, createUserContext(r, result.UserID, result.Role)) } }) } ================================================ FILE: api/middleware_gzip.go ================================================ package main import ( "compress/gzip" "io" "net/http" "strings" ) type gzipResponseWriter struct { io.Writer http.ResponseWriter } // Use the Writer part of gzipResponseWriter to write the output. func (w gzipResponseWriter) Write(b []byte) (int, error) { return w.Writer.Write(b) } func makeGzipHandler(fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Check if the client can accept the gzip encoding. if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { // The client cannot accept it, so return the output // uncompressed. fn(w, r) return } // Set the HTTP header indicating encoding. w.Header().Set("Content-Encoding", "gzip") gz := gzip.NewWriter(w) defer gz.Close() fn(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r) } } ================================================ FILE: api/middleware_lastRequestTime.go ================================================ package main import ( "net/http" "time" ) // Middleware to update lastRequest time func UpdateLastRequestTime(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Update lastRequest time lastRequest = time.Now() // Call next middleware/handler next.ServeHTTP(w, r) }) } ================================================ FILE: api/middleware_rateLimit.go ================================================ package main import ( "fmt" "net/http" "strings" "github.com/swuecho/chat_backend/sqlc_queries" ) // This function returns a middleware that limits requests from each user by their ID. func RateLimitByUserID(q *sqlc_queries.Queries) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get the user ID from the request, e.g. from a JWT token. path := r.URL.Path if strings.HasSuffix(path, "/chat") || strings.HasSuffix(path, "/chat_stream") || strings.HasSuffix(path, "/chatbot") { ctx := r.Context() userIDInt, err := getUserID(ctx) // role := ctx.Value(roleContextKey).(string) if err != nil { apiErr := ErrAuthInvalidCredentials apiErr.Detail = "User identification required for rate limiting" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } messageCount, err := q.GetChatMessagesCount(r.Context(), int32(userIDInt)) if err != nil { apiErr := ErrInternalUnexpected apiErr.Detail = "Could not get message count for rate limiting" apiErr.DebugInfo = err.Error() RespondWithAPIError(w, apiErr) return } maxRate, err := q.GetRateLimit(r.Context(), int32(userIDInt)) if err != nil { maxRate = int32(appConfig.OPENAI.RATELIMIT) } if messageCount >= int64(maxRate) { apiErr := ErrTooManyRequests apiErr.Detail = fmt.Sprintf("Rate limit exceeded: messageCount=%d, maxRate=%d", messageCount, maxRate) RespondWithAPIError(w, apiErr) return } } // Call the next handler. next.ServeHTTP(w, r) }) } } ================================================ FILE: api/middleware_validation.go ================================================ package main import ( "bytes" "context" "encoding/json" "io" "net/http" "regexp" "strings" "time" "unicode/utf8" log "github.com/sirupsen/logrus" ) // Common validation patterns var ( validationEmailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) validationUuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) ) // ValidationConfig defines validation rules for API endpoints type ValidationConfig struct { MaxBodySize int64 // Maximum request body size RequiredFields []string // Required JSON fields FieldValidators map[string]FieldValidator // Custom field validators AllowedMethods []string // Allowed HTTP methods SkipBodyBuffer bool // Skip body buffering for large requests (disables field validation) } // FieldValidator defines a validation function for a specific field type FieldValidator func(value interface{}) error // Common field validators func ValidateEmail(value interface{}) error { email, ok := value.(string) if !ok { return ErrValidationInvalidInput("email must be a string") } if !validationEmailRegex.MatchString(email) { return ErrValidationInvalidInput("invalid email format") } if len(email) > 254 { return ErrValidationInvalidInput("email too long") } return nil } func ValidateUUID(value interface{}) error { uuid, ok := value.(string) if !ok { return ErrValidationInvalidInput("UUID must be a string") } if !validationUuidRegex.MatchString(uuid) { return ErrValidationInvalidInput("invalid UUID format") } return nil } func ValidateStringLength(min, max int) FieldValidator { return func(value interface{}) error { str, ok := value.(string) if !ok { return ErrValidationInvalidInput("value must be a string") } if !utf8.ValidString(str) { return ErrValidationInvalidInput("invalid UTF-8 string") } if len(str) < min { return ErrValidationInvalidInput("string too short") } if len(str) > max { return ErrValidationInvalidInput("string too long") } return nil } } func ValidateNonEmpty(value interface{}) error { str, ok := value.(string) if !ok { return ErrValidationInvalidInput("value must be a string") } if strings.TrimSpace(str) == "" { return ErrValidationInvalidInput("value cannot be empty") } return nil } // ValidationMiddleware creates a validation middleware with the given config func ValidationMiddleware(config ValidationConfig) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Start timing for performance monitoring start := time.Now() // Validate HTTP method if len(config.AllowedMethods) > 0 { methodAllowed := false for _, method := range config.AllowedMethods { if r.Method == method { methodAllowed = true break } } if !methodAllowed { log.WithFields(log.Fields{ "method": r.Method, "path": r.URL.Path, "ip": r.RemoteAddr, }).Warn("Method not allowed") RespondWithAPIError(w, APIError{ HTTPCode: http.StatusMethodNotAllowed, Code: ErrValidation + "_100", Message: "Method not allowed", }) return } } // Skip validation for GET requests without body if r.Method == "GET" || r.Method == "DELETE" { next.ServeHTTP(w, r) return } // Validate content type for requests with body contentType := r.Header.Get("Content-Type") if !strings.Contains(contentType, "application/json") && r.ContentLength > 0 { log.WithFields(log.Fields{ "content_type": contentType, "path": r.URL.Path, "ip": r.RemoteAddr, }).Warn("Invalid content type") RespondWithAPIError(w, APIError{ HTTPCode: http.StatusUnsupportedMediaType, Code: ErrValidation + "_101", Message: "Content-Type must be application/json", }) return } // Check content length if config.MaxBodySize > 0 && r.ContentLength > config.MaxBodySize { log.WithFields(log.Fields{ "content_length": r.ContentLength, "max_size": config.MaxBodySize, "path": r.URL.Path, "ip": r.RemoteAddr, }).Warn("Request body too large") RespondWithAPIError(w, APIError{ HTTPCode: http.StatusRequestEntityTooLarge, Code: ErrValidation + "_102", Message: "Request body too large", }) return } // For requests without body, skip JSON validation if r.ContentLength == 0 { next.ServeHTTP(w, r) return } // Skip body buffering for large requests or when explicitly configured if config.SkipBodyBuffer { // Just validate content length and skip field validation if config.MaxBodySize > 0 && r.ContentLength > config.MaxBodySize { log.WithFields(log.Fields{ "content_length": r.ContentLength, "max_size": config.MaxBodySize, "path": r.URL.Path, "ip": r.RemoteAddr, }).Warn("Request body exceeds size limit") RespondWithAPIError(w, APIError{ HTTPCode: http.StatusRequestEntityTooLarge, Code: ErrValidation + "_102", Message: "Request body too large", }) return } next.ServeHTTP(w, r) return } // Create a limited reader to prevent memory exhaustion var limitedReader io.Reader = r.Body if config.MaxBodySize > 0 { limitedReader = io.LimitReader(r.Body, config.MaxBodySize+1) } // Read with streaming approach using a buffer var bodyBuffer bytes.Buffer written, err := io.CopyN(&bodyBuffer, limitedReader, config.MaxBodySize+1) r.Body.Close() if err != nil && err != io.EOF { log.WithError(err).WithFields(log.Fields{ "path": r.URL.Path, "ip": r.RemoteAddr, }).Error("Failed to read request body") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to read request body")) return } // Check if body exceeds limit if config.MaxBodySize > 0 && written > config.MaxBodySize { log.WithFields(log.Fields{ "body_size": written, "max_size": config.MaxBodySize, "path": r.URL.Path, "ip": r.RemoteAddr, }).Warn("Request body exceeds size limit") RespondWithAPIError(w, APIError{ HTTPCode: http.StatusRequestEntityTooLarge, Code: ErrValidation + "_102", Message: "Request body too large", }) return } body := bodyBuffer.Bytes() // Parse JSON body var jsonData map[string]interface{} if len(body) > 0 { if err := json.Unmarshal(body, &jsonData); err != nil { log.WithError(err).WithFields(log.Fields{ "path": r.URL.Path, "ip": r.RemoteAddr, }).Warn("Invalid JSON in request body") RespondWithAPIError(w, ErrValidationInvalidInput("Invalid JSON format").WithDebugInfo(err.Error())) return } // Validate required fields for _, field := range config.RequiredFields { if _, exists := jsonData[field]; !exists { log.WithFields(log.Fields{ "missing_field": field, "path": r.URL.Path, "ip": r.RemoteAddr, }).Warn("Missing required field") RespondWithAPIError(w, ErrValidationInvalidInput("Missing required field: "+field)) return } } // Run field validators for fieldName, validator := range config.FieldValidators { if value, exists := jsonData[fieldName]; exists { if err := validator(value); err != nil { log.WithError(err).WithFields(log.Fields{ "field": fieldName, "path": r.URL.Path, "ip": r.RemoteAddr, }).Warn("Field validation failed") RespondWithAPIError(w, WrapError(err, "Validation failed for field: "+fieldName)) return } } } } // Restore body for next handler r.Body = io.NopCloser(bytes.NewReader(body)) // Add validation context ctx := context.WithValue(r.Context(), "validation_duration", time.Since(start)) r = r.WithContext(ctx) log.WithFields(log.Fields{ "path": r.URL.Path, "method": r.Method, "duration": time.Since(start), "ip": r.RemoteAddr, }).Debug("Request validation completed") next.ServeHTTP(w, r) }) } } // Predefined validation configs for common endpoints var ( AuthValidationConfig = ValidationConfig{ MaxBodySize: 1024 * 10, // 10KB RequiredFields: []string{"email", "password"}, FieldValidators: map[string]FieldValidator{ "email": ValidateEmail, "password": ValidateStringLength(8, 128), }, AllowedMethods: []string{"POST"}, } ChatValidationConfig = ValidationConfig{ MaxBodySize: 1024 * 100, // 100KB RequiredFields: []string{"prompt"}, FieldValidators: map[string]FieldValidator{ "prompt": ValidateStringLength(1, 10000), "session_uuid": ValidateUUID, "chat_uuid": ValidateUUID, }, AllowedMethods: []string{"POST"}, } FileUploadValidationConfig = ValidationConfig{ MaxBodySize: 32 * 1024 * 1024, // 32MB AllowedMethods: []string{"POST", "GET", "DELETE"}, SkipBodyBuffer: true, // Skip buffering for large file uploads } GeneralValidationConfig = ValidationConfig{ MaxBodySize: 1024 * 50, // 50KB AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, } ) ================================================ FILE: api/model_claude3_service.go ================================================ package main import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "time" openai "github.com/sashabaranov/go-openai" claude "github.com/swuecho/chat_backend/llm/claude" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) // ClaudeResponse represents the response structure from Claude API type ClaudeResponse struct { Completion string `json:"completion"` Stop string `json:"stop"` StopReason string `json:"stop_reason"` Truncated bool `json:"truncated"` LogID string `json:"log_id"` Model string `json:"model"` Exception any `json:"exception"` } // Claude3 ChatModel implementation type Claude3ChatModel struct { h *ChatHandler } func (m *Claude3ChatModel) Stream(w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_compeletion_messages []models.Message, chatUuid string, regenerate bool, stream bool) (*models.LLMAnswer, error) { // Get chat model configuration chatModel, err := GetChatModel(m.h.service.q, chatSession.Model) if err != nil { return nil, err } // Get chat files if any chatFiles, err := GetChatFiles(m.h.chatfileService.q, chatSession.Uuid) if err != nil { return nil, err } // create a new strings.Builder // iterate through the messages and format them // print the user's question // convert assistant's response to json format // "messages": [ // {"role": "user", "content": "Hello, world"} // ] // first message is user instead of system var messages []openai.ChatCompletionMessage if len(chat_compeletion_messages) > 1 { // first message used as system message // messages start with second message // drop the first assistant message if it is an assistant message claude_messages := chat_compeletion_messages[1:] if len(claude_messages) > 0 && claude_messages[0].Role == "assistant" { claude_messages = claude_messages[1:] } messages = messagesToOpenAIMesages(claude_messages, chatFiles) } else { // only system message, return and do nothing return nil, ErrSystemMessageError } // Prepare request payload jsonData := map[string]any{ "system": chat_compeletion_messages[0].Content, "model": chatSession.Model, "messages": messages, "max_tokens": chatSession.MaxTokens, "temperature": chatSession.Temperature, "top_p": chatSession.TopP, "stream": stream, } jsonValue, err := json.Marshal(jsonData) if err != nil { return nil, ErrValidationInvalidInputGeneric.WithDetail("failed to marshal request payload").WithDebugInfo(err.Error()) } // Get request context for cancellation support ctx := m.h.GetRequestContext() // Create HTTP request with context req, err := http.NewRequestWithContext(ctx, "POST", chatModel.Url, bytes.NewBuffer(jsonValue)) if err != nil { return nil, ErrClaudeRequestFailed.WithDetail("failed to create HTTP request").WithDebugInfo(err.Error()) } // add headers to the request apiKey := os.Getenv(chatModel.ApiAuthKey) if apiKey == "" { return nil, ErrAuthInvalidCredentials.WithDetail(fmt.Sprintf("missing API key for model %s", chatSession.Model)) } authHeaderName := chatModel.ApiAuthHeader if authHeaderName != "" { req.Header.Set(authHeaderName, apiKey) } req.Header.Set("Content-Type", "application/json") req.Header.Set("anthropic-version", "2023-06-01") if !stream { req.Header.Set("Accept", "application/json") client := http.Client{ Timeout: 5 * time.Minute, } llmAnswer, err := doGenerateClaude3(ctx, client, req) if err != nil { return nil, ErrClaudeRequestFailed.WithDetail("failed to generate response").WithDebugInfo(err.Error()) } answerResponse := constructChatCompletionStreamResponse(llmAnswer.AnswerId, llmAnswer.Answer) data, err := json.Marshal(answerResponse) if err != nil { return nil, ErrInternalUnexpected.WithDetail("failed to marshal response").WithDebugInfo(err.Error()) } if _, err := fmt.Fprint(w, string(data)); err != nil { return nil, ErrClaudeResponseFaild.WithDetail("failed to write response").WithDebugInfo(err.Error()) } return llmAnswer, nil } // Handle streaming response req.Header.Set("Accept", "text/event-stream") req.Header.Set("Cache-Control", "no-cache") req.Header.Set("Connection", "keep-alive") llmAnswer, err := m.h.chatStreamClaude3(ctx, w, req, chatUuid, regenerate) if err != nil { return nil, ErrClaudeStreamFailed.WithDetail("failed to stream response").WithDebugInfo(err.Error()) } return llmAnswer, nil } func doGenerateClaude3(ctx context.Context, client http.Client, req *http.Request) (*models.LLMAnswer, error) { resp, err := client.Do(req) if err != nil { return nil, ErrClaudeRequestFailed.WithMessage("Failed to process Claude request").WithDebugInfo(err.Error()) } // Unmarshal directly from resp.Body var message claude.Response if err := json.NewDecoder(resp.Body).Decode(&message); err != nil { return nil, ErrClaudeInvalidResponse.WithMessage("Failed to unmarshal Claude response").WithDebugInfo(err.Error()) } defer resp.Body.Close() uuid := message.ID firstMessage := message.Content[0].Text return &models.LLMAnswer{ AnswerId: uuid, Answer: firstMessage, }, nil } // claude-3-opus-20240229 // claude-3-sonnet-20240229 // claude-3-haiku-20240307 func (h *ChatHandler) chatStreamClaude3(ctx context.Context, w http.ResponseWriter, req *http.Request, chatUuid string, regenerate bool) (*models.LLMAnswer, error) { // create the http client and send the request client := &http.Client{ Timeout: 5 * time.Minute, } resp, err := client.Do(req) if err != nil { return nil, ErrClaudeRequestFailed.WithMessage("Failed to process Claude streaming request").WithDebugInfo(err.Error()) } // Use smaller buffer for more responsive streaming ioreader := bufio.NewReaderSize(resp.Body, 1024) // read the response body defer resp.Body.Close() // loop over the response body and print data flusher, err := setupSSEStream(w) if err != nil { return nil, APIError{ HTTPCode: http.StatusInternalServerError, Code: "STREAM_UNSUPPORTED", Message: "Streaming unsupported by client", } } // Flush immediately to establish connection flusher.Flush() var answer string answer_id := GenerateAnswerID(chatUuid, regenerate) var headerData = []byte("data: ") count := 0 for { // Check if client disconnected or context was cancelled select { case <-ctx.Done(): log.Printf("Claude stream cancelled by client: %v", ctx.Err()) // Return current accumulated content when cancelled return &models.LLMAnswer{Answer: answer, AnswerId: answer_id}, nil default: } count++ // prevent infinite loop if count > 10000 { break } line, err := ioreader.ReadBytes('\n') log.Printf("%+v", string(line)) if err != nil { if errors.Is(err, io.EOF) { if bytes.HasPrefix(line, []byte("{\"type\":\"error\"")) { log.Println(string(line)) err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: NewUUID(), Content: string(line), IsFinal: true, }) if err != nil { log.Printf("Failed to flush error response: %v", err) } } fmt.Println("End of stream reached") return nil, err } return nil, err } line = bytes.TrimPrefix(line, headerData) if bytes.HasPrefix(line, []byte("event: message_stop")) { // stream.isFinished = true // No need to send full content at the end since we're sending deltas break } if bytes.HasPrefix(line, []byte("{\"type\":\"error\"")) { log.Println(string(line)) return nil, ErrClaudeStreamFailed.WithMessage("Error in Claude API response").WithDebugInfo(string(line)) } if answer_id == "" { answer_id = NewUUID() } if bytes.HasPrefix(line, []byte("{\"type\":\"content_block_start\"")) { answer = claude.AnswerFromBlockStart(line) err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: answer, IsFinal: false, }) if err != nil { log.Printf("Failed to flush content block start: %v", err) } } if bytes.HasPrefix(line, []byte("{\"type\":\"content_block_delta\"")) { delta := claude.AnswerFromBlockDelta(line) answer += delta // Still accumulate for final answer storage // Send only the delta content err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: delta, IsFinal: false, }) if err != nil { log.Printf("Failed to flush content block delta: %v", err) } } // Flush after every iteration to ensure immediate delivery // This prevents data from being held in buffers if count%3 == 0 { flusher.Flush() } } return &models.LLMAnswer{ Answer: answer, AnswerId: answer_id, }, nil } ================================================ FILE: api/model_completion_service.go ================================================ package main import ( "context" "encoding/json" "errors" "io" "log" "net/http" "strings" "github.com/rotisserie/eris" openai "github.com/sashabaranov/go-openai" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) // CompletionChatModel implements ChatModel interface for OpenAI completion models type CompletionChatModel struct { h *ChatHandler } // Stream implements the ChatModel interface for completion model scenarios func (m *CompletionChatModel) Stream(w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_completion_messages []models.Message, chatUuid string, regenerate bool, stream bool) (*models.LLMAnswer, error) { // Get request context for cancellation support ctx := m.h.GetRequestContext() return m.completionStream(ctx, w, chatSession, chat_completion_messages, chatUuid, regenerate, stream) } // completionStream handles streaming for OpenAI completion models func (m *CompletionChatModel) completionStream(ctx context.Context, w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_completion_messages []models.Message, chatUuid string, regenerate bool, _ bool) (*models.LLMAnswer, error) { // Check per chat_model rate limit openAIRateLimiter.Wait(context.Background()) exceedPerModeRateLimitOrError := m.h.CheckModelAccess(w, chatSession.Uuid, chatSession.Model, chatSession.UserID) if exceedPerModeRateLimitOrError { return nil, eris.New("exceed per mode rate limit") } // Get chat model configuration chatModel, err := GetChatModel(m.h.service.q, chatSession.Model) if err != nil { RespondWithAPIError(w, createAPIError(ErrResourceNotFound(""), "chat model "+chatSession.Model, "")) return nil, err } // Generate OpenAI client configuration config, err := genOpenAIConfig(*chatModel) if err != nil { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to generate OpenAI configuration", err.Error())) return nil, err } client := openai.NewClientWithConfig(config) // Get the latest message content as prompt prompt := chat_completion_messages[len(chat_completion_messages)-1].Content // Create completion request N := chatSession.N req := openai.CompletionRequest{ Model: chatSession.Model, Temperature: float32(chatSession.Temperature), TopP: float32(chatSession.TopP), N: int(N), Prompt: prompt, Stream: true, } // Create completion stream with timeout ctx, cancel := context.WithTimeout(context.Background(), DefaultRequestTimeout) defer cancel() stream, err := client.CreateCompletionStream(ctx, req) if err != nil { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to create completion stream", err.Error())) return nil, err } defer stream.Close() // Setup SSE streaming flusher, err := setupSSEStream(w) if err != nil { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Streaming unsupported by client", err.Error())) return nil, err } var answer string answer_id := GenerateAnswerID(chatUuid, regenerate) textBuffer := newTextBuffer(N, "```\n"+prompt, "\n```\n") // Process streaming response for { // Check if client disconnected or context was cancelled select { case <-ctx.Done(): log.Printf("Completion stream cancelled by client: %v", ctx.Err()) // Return current accumulated content when cancelled return &models.LLMAnswer{Answer: answer, AnswerId: answer_id}, nil default: } response, err := stream.Recv() if errors.Is(err, io.EOF) { // Send the final message if len(answer) > 0 { err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: answer, IsFinal: true, }) if err != nil { log.Printf("Failed to flush final response: %v", err) } } // Include debug information if enabled if chatSession.Debug { req_j, _ := json.Marshal(req) log.Println(string(req_j)) answer = answer + "\n" + string(req_j) err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: answer, IsFinal: true, }) if err != nil { log.Printf("Failed to flush debug response: %v", err) } } break } if err != nil { RespondWithAPIError(w, ErrChatStreamFailed.WithMessage("Stream error occurred").WithDebugInfo(err.Error())) return nil, err } // Process response chunk textIdx := response.Choices[0].Index delta := response.Choices[0].Text textBuffer.appendByIndex(textIdx, delta) if chatSession.Debug { log.Printf("%d: %s", textIdx, delta) } if answer_id == "" { answer_id = response.ID } // Concatenate all string builders into a single string answer = textBuffer.String("\n\n") // Determine when to flush the response perWordStreamLimit := getPerWordStreamLimit() if strings.HasSuffix(delta, "\n") || len(answer) < perWordStreamLimit { if len(answer) == 0 { log.Print(ErrorNoContent) } else { err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: answer, IsFinal: false, }) if err != nil { log.Printf("Failed to flush response: %v", err) } } } } return &models.LLMAnswer{AnswerId: answer_id, Answer: answer}, nil } ================================================ FILE: api/model_custom_service.go ================================================ package main import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "strings" claude "github.com/swuecho/chat_backend/llm/claude" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) // CustomModelResponse represents the response structure for custom models type CustomModelResponse struct { Completion string `json:"completion"` Stop string `json:"stop"` StopReason string `json:"stop_reason"` Truncated bool `json:"truncated"` LogID string `json:"log_id"` Model string `json:"model"` Exception any `json:"exception"` } // CustomChatModel implements ChatModel interface for custom model providers type CustomChatModel struct { h *ChatHandler } // Stream implements the ChatModel interface for custom model scenarios func (m *CustomChatModel) Stream(w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_completion_messages []models.Message, chatUuid string, regenerate bool, stream bool) (*models.LLMAnswer, error) { // Get request context for cancellation support ctx := m.h.GetRequestContext() return m.customChatStream(ctx, w, chatSession, chat_completion_messages, chatUuid, regenerate) } // customChatStream handles streaming for custom model providers func (m *CustomChatModel) customChatStream(ctx context.Context, w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_completion_messages []models.Message, chatUuid string, regenerate bool) (*models.LLMAnswer, error) { // Get chat model configuration chat_model, err := GetChatModel(m.h.service.q, chatSession.Model) if err != nil { RespondWithAPIError(w, createAPIError(ErrResourceNotFound(""), "chat model: "+chatSession.Model, "")) return nil, err } // Get API key from environment apiKey := os.Getenv(chat_model.ApiAuthKey) url := chat_model.Url // Format messages for the custom model prompt := claude.FormatClaudePrompt(chat_completion_messages) // Create request payload jsonData := map[string]any{ "prompt": prompt, "model": chatSession.Model, "max_tokens_to_sample": chatSession.MaxTokens, "temperature": chatSession.Temperature, "stop_sequences": []string{"\n\nHuman:"}, "stream": true, } // Marshal request data jsonValue, _ := json.Marshal(jsonData) // Create HTTP request with context req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonValue)) if err != nil { RespondWithAPIError(w, createAPIError(ErrChatRequestFailed, "Failed to create custom model request", err.Error())) return nil, err } // Set authentication header if configured authHeaderName := chat_model.ApiAuthHeader if authHeaderName != "" { req.Header.Set(authHeaderName, apiKey) } // Set request headers SetStreamingHeaders(req) // Send HTTP request client := &http.Client{} resp, err := client.Do(req) if err != nil { RespondWithAPIError(w, createAPIError(ErrChatRequestFailed, "Failed to send custom model request", err.Error())) return nil, err } defer resp.Body.Close() // Setup streaming response ioreader := bufio.NewReader(resp.Body) flusher, err := setupSSEStream(w) if err != nil { RespondWithAPIError(w, createAPIError(APIError{ HTTPCode: http.StatusInternalServerError, Code: "STREAM_UNSUPPORTED", Message: "Streaming unsupported by client", }, "", err.Error())) return nil, err } var answer string var answer_id string var lastFlushLength int answer_id = GenerateAnswerID(chatUuid, regenerate) headerData := []byte("data: ") count := 0 // Process streaming response for { // Check if client disconnected or context was cancelled select { case <-ctx.Done(): log.Printf("Custom model stream cancelled by client: %v", ctx.Err()) // Return current accumulated content when cancelled return &models.LLMAnswer{Answer: answer, AnswerId: answer_id}, nil default: } count++ // Prevent infinite loop if count > MaxStreamingLoopIterations { break } line, err := ioreader.ReadBytes('\n') if err != nil { if errors.Is(err, io.EOF) { fmt.Println(ErrorEndOfStream) break } return nil, err } if !bytes.HasPrefix(line, headerData) { continue } line = bytes.TrimPrefix(line, headerData) if bytes.HasPrefix(line, []byte("[DONE]")) { fmt.Println(ErrorDoneBreak) break } if answer_id == "" { answer_id = NewUUID() } var response CustomModelResponse _ = json.Unmarshal(line, &response) answer = response.Completion // Determine when to flush the response shouldFlush := strings.Contains(answer, "\n") || len(answer) < SmallAnswerThreshold || (len(answer)-lastFlushLength) >= FlushCharacterThreshold if shouldFlush { err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: answer, IsFinal: false, }) if err != nil { log.Printf("Failed to flush response: %v", err) } lastFlushLength = len(answer) } } return &models.LLMAnswer{ Answer: answer, AnswerId: answer_id, }, nil } ================================================ FILE: api/model_gemini_service.go ================================================ package main import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "strings" "time" "github.com/swuecho/chat_backend/llm/gemini" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) // Generated by curl-to-Go: https://mholt.github.io/curl-to-go // curl https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=$API_KEY \ // -H 'Content-Type: application/json' \ // -X POST \ // -d '{ // "contents": [{ // "parts":[{ // "text": "Write a story about a magic backpack."}]}]}' 2> /dev/null // GeminiClient handles communication with the Gemini API type GeminiClient struct { client *http.Client } // NewGeminiClient creates a new Gemini API client func NewGeminiClient() *GeminiClient { return &GeminiClient{ client: &http.Client{Timeout: 5 * time.Minute}, } } // Gemini ChatModel implementation type GeminiChatModel struct { h *ChatHandler client *GeminiClient } func NewGeminiChatModel(h *ChatHandler) *GeminiChatModel { return &GeminiChatModel{ h: h, client: NewGeminiClient(), } } func (m *GeminiChatModel) Stream(w http.ResponseWriter, chatSession sqlc_queries.ChatSession, messages []models.Message, chatUuid string, regenerate bool, stream bool) (*models.LLMAnswer, error) { answerID := GenerateAnswerID(chatUuid, regenerate) chatFiles, err := GetChatFiles(m.h.chatfileService.q, chatSession.Uuid) if err != nil { return nil, err } payloadBytes, err := gemini.GenGemminPayload(messages, chatFiles) if err != nil { return nil, ErrInternalUnexpected.WithMessage("Failed to generate Gemini payload").WithDebugInfo(err.Error()) } // Get request context for cancellation support ctx := m.h.GetRequestContext() url := gemini.BuildAPIURL(chatSession.Model, stream) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(payloadBytes)) if err != nil { return nil, ErrInternalUnexpected.WithMessage("Failed to create Gemini API request").WithDebugInfo(err.Error()) } req.Header.Set("Content-Type", "application/json") if stream { return m.handleStreamResponse(ctx, w, req, answerID) } llmAnswer, err := gemini.HandleRegularResponse(*m.client.client, req) if err != nil { return nil, ErrInternalUnexpected.WithMessage("Failed to generate regular response").WithDebugInfo(err.Error()) } if llmAnswer == nil { return nil, ErrInternalUnexpected.WithMessage("Empty response from Gemini") } llmAnswer.AnswerId = answerID response := constructChatCompletionStreamResponse(answerID, llmAnswer.Answer) data, _ := json.Marshal(response) fmt.Fprint(w, string(data)) return llmAnswer, err } func GenerateChatTitle(ctx context.Context, model, chatText string) (string, error) { // Validate API key apiKey := os.Getenv("GEMINI_API_KEY") if apiKey == "" { return "", ErrInternalUnexpected.WithMessage("GEMINI_API_KEY environment variable not set") } // Validate input if strings.TrimSpace(chatText) == "" { return "", ErrValidationInvalidInput("chat text cannot be empty") } // Create properly formatted Gemini messages messages := []models.Message{ { Role: "user", Content: `Generate a short title (3-6 words) for this conversation. Output ONLY the title text, no quotes, no markdown, no prefixes like "Title:". Example: "Python list comprehension guide"`, }, { Role: "user", Content: chatText, }, } // Generate proper Gemini payload payloadBytes, err := gemini.GenGemminPayload(messages, nil) if err != nil { return "", ErrInternalUnexpected.WithMessage("Failed to generate Gemini payload").WithDebugInfo(err.Error()) } // Build URL with proper API key url := gemini.BuildAPIURL(model, false) req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) if err != nil { return "", ErrInternalUnexpected.WithMessage("Failed to create Gemini API request").WithDebugInfo(err.Error()) } req.Header.Set("Content-Type", "application/json") answer, err := gemini.HandleRegularResponse(http.Client{Timeout: 1 * time.Minute}, req) if err != nil { return "", ErrInternalUnexpected.WithMessage("Failed to handle Gemini response").WithDebugInfo(err.Error()) } // Validate and clean up response if answer == nil || answer.Answer == "" { return "", ErrInternalUnexpected.WithMessage("Empty response from Gemini") } title := strings.TrimSpace(answer.Answer) title = strings.Trim(title, `"`) title = strings.Trim(title, `*`) title = strings.Trim(title, `#`) // Remove common prefixes title = strings.TrimPrefix(title, "Title:") title = strings.TrimPrefix(title, "title:") title = strings.TrimPrefix(title, "Title: ") title = strings.TrimPrefix(title, "title: ") title = strings.TrimSpace(title) // Remove any remaining markdown or special characters at the start for strings.HasPrefix(title, "#") || strings.HasPrefix(title, "-") || strings.HasPrefix(title, "*") { title = strings.TrimLeft(title, "#-* ") title = strings.TrimSpace(title) } if title == "" { return "", ErrInternalUnexpected.WithMessage("Invalid title generated") } // Truncate and return return firstN(title, 100), nil } func (m *GeminiChatModel) handleStreamResponse(ctx context.Context, w http.ResponseWriter, req *http.Request, answerID string) (*models.LLMAnswer, error) { resp, err := m.client.client.Do(req) if err != nil { return nil, ErrInternalUnexpected.WithMessage("Failed to send Gemini API request").WithDebugInfo(err.Error()) } defer resp.Body.Close() flusher, err := setupSSEStream(w) if err != nil { return nil, APIError{ HTTPCode: http.StatusInternalServerError, Code: "STREAM_UNSUPPORTED", Message: "Streaming unsupported by client", } } var answer string log.Println(resp.StatusCode) if resp.StatusCode != http.StatusOK { errorBody, _ := io.ReadAll(resp.Body) log.Println(string(errorBody)) var apiError gemini.GoogleApiError if json.Unmarshal(errorBody, &apiError) == nil && apiError.Error.Message != "" { log.Printf("API returned non-200 status: %d %s. Error: %s", resp.StatusCode, http.StatusText(resp.StatusCode), &apiError) } else { log.Printf("API returned non-200 status: %d %s. Body: %s", resp.StatusCode, http.StatusText(resp.StatusCode), string(errorBody)) } return nil, APIError{ HTTPCode: apiError.Error.Code, Code: apiError.Error.Status, Message: apiError.Error.Message, } } ioreader := bufio.NewReader(resp.Body) headerData := []byte("data: ") for count := 0; count < 10000; count++ { // Check if client disconnected or context was cancelled select { case <-ctx.Done(): log.Printf("Gemini stream cancelled by client: %v", ctx.Err()) // Return current accumulated content when cancelled return &models.LLMAnswer{Answer: answer, AnswerId: answerID}, nil default: } line, err := ioreader.ReadBytes('\n') if err != nil { if errors.Is(err, io.EOF) { return &models.LLMAnswer{ Answer: answer, AnswerId: answerID, }, nil } return nil, ErrInternalUnexpected.WithMessage("Error reading stream").WithDebugInfo(err.Error()) } if !bytes.HasPrefix(line, headerData) { continue } line = bytes.TrimPrefix(line, headerData) if len(line) > 0 { delta := gemini.ParseRespLineDelta(line) answer += delta // Accumulate delta for final answer storage // Send only the delta content if len(delta) > 0 { err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answerID, Content: delta, IsFinal: false, }) if err != nil { log.Printf("Failed to flush response: %v", err) } } } } return &models.LLMAnswer{ AnswerId: answerID, Answer: answer, }, nil } ================================================ FILE: api/model_ollama_service.go ================================================ package main import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "strings" "time" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) // OllamaResponse represents the response structure from Ollama API type OllamaResponse struct { Model string `json:"model"` CreatedAt time.Time `json:"created_at"` Done bool `json:"done"` Message models.Message `json:"message"` TotalDuration int64 `json:"total_duration"` LoadDuration int64 `json:"load_duration"` PromptEvalCount int `json:"prompt_eval_count"` PromptEvalDuration int64 `json:"prompt_eval_duration"` EvalCount int `json:"eval_count"` EvalDuration int64 `json:"eval_duration"` } // Ollama ChatModel implementation type OllamaChatModel struct { h *ChatHandler } func (m *OllamaChatModel) Stream(w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_compeletion_messages []models.Message, chatUuid string, regenerate bool, stream bool) (*models.LLMAnswer, error) { // Get request context for cancellation support ctx := m.h.GetRequestContext() return m.h.chatOllamStream(ctx, w, chatSession, chat_compeletion_messages, chatUuid, regenerate) } func (h *ChatHandler) chatOllamStream(ctx context.Context, w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_compeletion_messages []models.Message, chatUuid string, regenerate bool) (*models.LLMAnswer, error) { // set the api key chatModel, err := GetChatModel(h.service.q, chatSession.Model) if err != nil { RespondWithAPIError(w, createAPIError(ErrResourceNotFound(""), "chat model: "+chatSession.Model, "")) return nil, err } jsonData := map[string]any{ "model": strings.Replace(chatSession.Model, "ollama-", "", 1), "messages": chat_compeletion_messages, } // convert data to json format jsonValue, _ := json.Marshal(jsonData) // create the request with context req, err := http.NewRequestWithContext(ctx, "POST", chatModel.Url, bytes.NewBuffer(jsonValue)) if err != nil { RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to make request").WithDebugInfo(err.Error())) return nil, err } // add headers to the request apiKey := os.Getenv(chatModel.ApiAuthKey) authHeaderName := chatModel.ApiAuthHeader if authHeaderName != "" { req.Header.Set(authHeaderName, apiKey) } req.Header.Set("Content-Type", "application/json") // set the streaming flag req.Header.Set("Accept", "text/event-stream") req.Header.Set("Cache-Control", "no-cache") req.Header.Set("Connection", "keep-alive") req.Header.Set("Access-Control-Allow-Origin", "*") // create the http client and send the request client := &http.Client{ Timeout: 5 * time.Minute, } resp, err := client.Do(req) if err != nil { RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("Failed to create chat completion stream").WithDebugInfo(err.Error())) return nil, err } ioreader := bufio.NewReader(resp.Body) // read the response body defer resp.Body.Close() // loop over the response body and print data flusher, err := setupSSEStream(w) if err != nil { RespondWithAPIError(w, APIError{ HTTPCode: http.StatusInternalServerError, Code: "STREAM_UNSUPPORTED", Message: "Streaming unsupported by client", }) return nil, err } var answer string answer_id := GenerateAnswerID(chatUuid, regenerate) count := 0 for { // Check if client disconnected or context was cancelled select { case <-ctx.Done(): log.Printf("Ollama stream cancelled by client: %v", ctx.Err()) // Return current accumulated content when cancelled return &models.LLMAnswer{Answer: answer, AnswerId: answer_id}, nil default: } count++ // prevent infinite loop if count > 10000 { break } line, err := ioreader.ReadBytes('\n') if err != nil { if errors.Is(err, io.EOF) { fmt.Println("End of stream reached") break // Exit loop if end of stream } return nil, err } var streamResp OllamaResponse err = json.Unmarshal(line, &streamResp) if err != nil { return nil, err } delta := strings.ReplaceAll(streamResp.Message.Content, "<0x0A>", "\n") answer += delta // Still accumulate for final answer storage if streamResp.Done { // stream.isFinished = true fmt.Println("DONE break") // No need to send full content at the end since we're sending deltas break } if answer_id == "" { answer_id = NewUUID() } // Send delta content immediately when available if len(delta) > 0 { err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: delta, IsFinal: false, }) if err != nil { log.Printf("Failed to flush response: %v", err) } } } return &models.LLMAnswer{ Answer: answer, AnswerId: answer_id, }, nil } ================================================ FILE: api/model_openai_service.go ================================================ package main import ( "context" "encoding/json" "errors" "fmt" "io" "log" "net/http" "strings" "time" "github.com/rotisserie/eris" openai "github.com/sashabaranov/go-openai" llm_openai "github.com/swuecho/chat_backend/llm/openai" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) // OpenAI ChatModel implementation type OpenAIChatModel struct { h *ChatHandler } func (m *OpenAIChatModel) Stream(w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chatCompletionMessages []models.Message, chatUuid string, regenerate bool, streamOutput bool) (*models.LLMAnswer, error) { openAIRateLimiter.Wait(context.Background()) exceedPerModeRateLimitOrError := m.h.CheckModelAccess(w, chatSession.Uuid, chatSession.Model, chatSession.UserID) if exceedPerModeRateLimitOrError { return nil, eris.New("exceed per mode rate limit") } chatModel, err := GetChatModel(m.h.service.q, chatSession.Model) if err != nil { return nil, err } config, err := genOpenAIConfig(*chatModel) log.Printf("%+v", config.String()) // print all config details if err != nil { return nil, ErrOpenAIConfigFailed.WithMessage("Failed to generate OpenAI config").WithDebugInfo(err.Error()) } chatFiles, err := GetChatFiles(m.h.chatfileService.q, chatSession.Uuid) if err != nil { return nil, err } openaiReq := NewChatCompletionRequest(chatSession, chatCompletionMessages, chatFiles, streamOutput) if len(openaiReq.Messages) <= 1 { return nil, ErrSystemMessageError } log.Printf("OpenAI request prepared - Model: %s, MessageCount: %d, Temperature: %.2f", openaiReq.Model, len(openaiReq.Messages), openaiReq.Temperature) client := openai.NewClientWithConfig(config) if streamOutput { return doChatStream(w, client, openaiReq, chatSession.N, chatUuid, regenerate, m.h) } else { return handleRegularResponse(w, client, openaiReq) } } func handleRegularResponse(w http.ResponseWriter, client *openai.Client, req openai.ChatCompletionRequest) (*models.LLMAnswer, error) { // check per chat_model limit ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() completion, err := client.CreateChatCompletion(ctx, req) if err != nil { log.Printf("fail to do request: %+v", err) return nil, ErrOpenAIRequestFailed.WithMessage("Failed to create chat completion").WithDebugInfo(err.Error()) } log.Printf("completion: %+v", completion) data, _ := json.Marshal(completion) fmt.Fprint(w, string(data)) return &models.LLMAnswer{Answer: completion.Choices[0].Message.Content, AnswerId: completion.ID}, nil } // doChatStream handles streaming chat completion responses from OpenAI // It properly manages thinking tags for models that support reasoning content func doChatStream(w http.ResponseWriter, client *openai.Client, req openai.ChatCompletionRequest, bufferLen int32, chatUuid string, regenerate bool, handler *ChatHandler) (*models.LLMAnswer, error) { // Use request context with timeout, but prioritize client cancellation baseCtx := context.Background() if handler != nil { baseCtx = handler.GetRequestContext() } ctx, cancel := context.WithTimeout(baseCtx, 5*time.Minute) defer cancel() log.Print("Creating OpenAI stream") stream, err := client.CreateChatCompletionStream(ctx, req) if err != nil { log.Printf("fail to do request: %+v", err) return nil, ErrOpenAIStreamFailed.WithMessage("Failed to create chat completion stream").WithDebugInfo(err.Error()) } defer func() { if err := stream.Close(); err != nil { log.Printf("Error closing OpenAI stream: %v", err) } }() // Setup Server-Sent Events (SSE) streaming flusher, err := setupSSEStream(w) if err != nil { return nil, APIError{ HTTPCode: http.StatusInternalServerError, Code: "STREAM_UNSUPPORTED", Message: "Streaming unsupported by client", } } // Initialize streaming state var answer_id string var hasReason bool // Whether we've detected any reasoning content var reasonTagOpened bool // Whether we've sent the opening tag var reasonTagClosed bool // Whether we've sent the closing tag // Ensure minimum buffer length if bufferLen == 0 { log.Println("Buffer length is 0, setting to 1") bufferLen = 1 } // Initialize buffers for accumulating content textBuffer := newTextBuffer(bufferLen, "", "") reasonBuffer := newTextBuffer(bufferLen, "\n\n", "\n\n\n\n") answer_id = GenerateAnswerID(chatUuid, regenerate) // Main streaming loop for { // Check if client disconnected or context was cancelled select { case <-ctx.Done(): log.Printf("Stream cancelled by client: %v", ctx.Err()) // Return current accumulated content when cancelled llmAnswer := models.LLMAnswer{Answer: textBuffer.String("\n"), AnswerId: answer_id} if hasReason { llmAnswer.ReasoningContent = reasonBuffer.String("\n") } return &llmAnswer, nil default: } rawLine, err := stream.RecvRaw() if err != nil { log.Printf("stream error: %+v", err) if errors.Is(err, io.EOF) { // Stream ended successfully - return accumulated content llmAnswer := models.LLMAnswer{Answer: textBuffer.String("\n"), AnswerId: answer_id} if hasReason { llmAnswer.ReasoningContent = reasonBuffer.String("\n") } return &llmAnswer, nil } else { log.Printf("Stream error: %v", err) return nil, ErrOpenAIStreamFailed.WithMessage("Stream error occurred").WithDebugInfo(err.Error()) } } // Parse the streaming response response := llm_openai.ChatCompletionStreamResponse{} err = json.Unmarshal(rawLine, &response) if err != nil { log.Printf("Could not unmarshal response: %v\n", err) continue } // Extract delta content from the response textIdx := response.Choices[0].Index delta := response.Choices[0].Delta // Accumulate content in buffers (for final answer construction) textBuffer.appendByIndex(textIdx, delta.Content) if len(delta.ReasoningContent) > 0 { hasReason = true reasonBuffer.appendByIndex(textIdx, delta.ReasoningContent) } // Set answer ID from response if not already set if answer_id == "" { answer_id = strings.TrimPrefix(response.ID, "chatcmpl-") } // Process and send delta content if len(delta.Content) > 0 || len(delta.ReasoningContent) > 0 { deltaToSend := processDelta(delta, &reasonTagOpened, &reasonTagClosed, hasReason) if len(deltaToSend) > 0 { log.Printf("delta: %s", deltaToSend) err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: deltaToSend, IsFinal: false, }) if err != nil { log.Printf("Failed to flush response: %v", err) } } } } } // processDelta handles the logic for processing delta content with thinking tags func processDelta(delta llm_openai.ChatCompletionStreamChoiceDelta, reasonTagOpened *bool, reasonTagClosed *bool, hasReason bool) string { var deltaToSend string if len(delta.ReasoningContent) > 0 { // Handle reasoning content if !*reasonTagOpened { // First time seeing reasoning content, add opening tag deltaToSend = "" + delta.ReasoningContent *reasonTagOpened = true } else { // Continue reasoning content deltaToSend = delta.ReasoningContent } } else if hasReason && !*reasonTagClosed { // We had reasoning content before and now we have regular content for the first time // Close the think tag first, then send the content deltaToSend = "" + delta.Content *reasonTagClosed = true } else { // Regular content without reasoning deltaToSend = delta.Content } return deltaToSend } // NewUserMessage creates a new OpenAI user message func NewUserMessage(content string) openai.ChatCompletionMessage { return openai.ChatCompletionMessage{Role: "user", Content: content} } // NewChatCompletionRequest creates an OpenAI chat completion request from session and messages func NewChatCompletionRequest(chatSession sqlc_queries.ChatSession, chatCompletionMessages []models.Message, chatFiles []sqlc_queries.ChatFile, streamOutput bool) openai.ChatCompletionRequest { openaiMessages := messagesToOpenAIMesages(chatCompletionMessages, chatFiles) for _, m := range openaiMessages { b, _ := m.MarshalJSON() log.Printf("messages: %+v\n", string(b)) } log.Printf("messages: %+v\n", openaiMessages) // Ensure TopP is always greater than 0 to prevent API validation errors topP := float32(chatSession.TopP) - 0.01 if topP <= 0 { topP = 0.01 // Minimum valid value } openaiReq := openai.ChatCompletionRequest{ Model: chatSession.Model, Messages: openaiMessages, Temperature: float32(chatSession.Temperature), TopP: topP, N: int(chatSession.N), Stream: streamOutput, } return openaiReq } ================================================ FILE: api/model_test_service.go ================================================ package main import ( "encoding/json" "log" "net/http" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) // TestChatModel implements ChatModel interface for testing purposes type TestChatModel struct { h *ChatHandler } // Stream implements the ChatModel interface for test scenarios func (m *TestChatModel) Stream(w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_completion_messages []models.Message, chatUuid string, regenerate bool, stream bool) (*models.LLMAnswer, error) { return m.chatStreamTest(w, chatSession, chat_completion_messages, chatUuid, regenerate) } // chatStreamTest handles test chat streaming with mock responses func (m *TestChatModel) chatStreamTest(w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_completion_messages []models.Message, chatUuid string, regenerate bool) (*models.LLMAnswer, error) { chatFiles, err := GetChatFiles(m.h.chatfileService.q, chatSession.Uuid) if err != nil { RespondWithAPIError(w, createAPIError(ErrInternalUnexpected, "Failed to get chat files", err.Error())) return nil, err } answer_id := GenerateAnswerID(chatUuid, regenerate) flusher, err := setupSSEStream(w) if err != nil { RespondWithAPIError(w, createAPIError(APIError{ HTTPCode: http.StatusInternalServerError, Code: "STREAM_UNSUPPORTED", Message: "Streaming unsupported by client", }, "", err.Error())) return nil, err } answer := "Hi, I am a chatbot. I can help you to find the best answer for your question. Please ask me a question." err = FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: answer, IsFinal: false, }) if err != nil { log.Printf("Failed to flush response: %v", err) } if chatSession.Debug { openai_req := NewChatCompletionRequest(chatSession, chat_completion_messages, chatFiles, false) req_j, _ := json.Marshal(openai_req) answer = answer + "\n" + string(req_j) err := FlushResponse(w, flusher, StreamingResponse{ AnswerID: answer_id, Content: answer, IsFinal: true, }) if err != nil { log.Printf("Failed to flush debug response: %v", err) } } return &models.LLMAnswer{ Answer: answer, AnswerId: answer_id, }, nil } ================================================ FILE: api/models/models.go ================================================ package models import ( "log" "github.com/pkoukk/tiktoken-go" ) func getTokenCount(content string) (int, error) { encoding := "cl100k_base" tke, err := tiktoken.GetEncoding(encoding) if err != nil { return 0, err } token := tke.Encode(content, nil, nil) num_tokens := len(token) return num_tokens, nil } type Message struct { Role string `json:"role"` Content string `json:"content"` tokenCount int32 } func (m Message) TokenCount() int32 { if m.tokenCount != 0 { return m.tokenCount } else { tokenCount, err := getTokenCount(m.Content) if err != nil { log.Println(err) } return int32(tokenCount) + 1 } } func (m *Message) SetTokenCount(tokenCount int32) *Message { m.tokenCount = tokenCount return m } type LLMAnswer struct { AnswerId string `json:"id"` Answer string `json:"answer"` ReasoningContent string `json:"reason_content"` } ================================================ FILE: api/models.go ================================================ package main import ( "net/http" "time" "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) type TokenResult struct { AccessToken string `json:"accessToken"` ExpiresIn int `json:"expiresIn"` } type ConversationRequest struct { UUID string `json:"uuid,omitempty"` ConversationID string `json:"conversationId,omitempty"` ParentMessageID string `json:"parentMessageId,omitempty"` } type RequestOption struct { Prompt string `json:"prompt,omitempty"` Options ConversationRequest `json:"options,omitempty"` } type Artifact struct { UUID string `json:"uuid"` Type string `json:"type"` // 'code', 'html', 'svg', 'mermaid', 'json', 'markdown' Title string `json:"title"` Content string `json:"content"` Language string `json:"language,omitempty"` // for code artifacts } type SimpleChatMessage struct { Uuid string `json:"uuid"` DateTime string `json:"dateTime"` Text string `json:"text"` Inversion bool `json:"inversion"` Error bool `json:"error"` Loading bool `json:"loading"` IsPin bool `json:"isPin"` IsPrompt bool `json:"isPrompt"` Artifacts []Artifact `json:"artifacts,omitempty"` } func (msg SimpleChatMessage) GetRole() string { var role string if msg.Inversion { role = "user" } else { role = "assistant" } return role } type SimpleChatSession struct { Uuid string `json:"uuid"` IsEdit bool `json:"isEdit"` Title string `json:"title"` MaxLength int `json:"maxLength"` Temperature float64 `json:"temperature"` TopP float64 `json:"topP"` N int32 `json:"n"` MaxTokens int32 `json:"maxTokens"` Debug bool `json:"debug"` Model string `json:"model"` SummarizeMode bool `json:"summarizeMode"` ArtifactEnabled bool `json:"artifactEnabled"` WorkspaceUuid string `json:"workspaceUuid"` } type ChatMessageResponse struct { Uuid string `json:"uuid"` ChatSessionUuid string `json:"chatSessionUuid"` Role string `json:"role"` Content string `json:"content"` Score float64 `json:"score"` UserID int32 `json:"userId"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` CreatedBy int32 `json:"createdBy"` UpdatedBy int32 `json:"updatedBy"` Artifacts []Artifact `json:"artifacts,omitempty"` } type ChatSessionResponse struct { Uuid string `json:"uuid"` Topic string `json:"topic"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` MaxLength int32 `json:"maxLength"` ArtifactEnabled bool `json:"artifactEnabled"` } type Pagination struct { Page int32 `json:"page"` Size int32 `json:"size"` Data []interface{} `json:"data"` Total int64 `json:"total"` } func (p *Pagination) Offset() int32 { return (p.Page - 1) * p.Size } // ChatModel interface type ChatModel interface { Stream(w http.ResponseWriter, chatSession sqlc_queries.ChatSession, chat_compeletion_messages []models.Message, chatUuid string, regenerate bool, stream bool) (*models.LLMAnswer, error) } ================================================ FILE: api/openai_test.go ================================================ package main import "testing" func Test_getModelBaseUrl(t *testing.T) { testCases := []struct { name string apiUrl string expected string }{ { name: "Base URL with v1 version", apiUrl: "https://api.openai-sb.com/v1/chat/completions", expected: "https://api.openai-sb.com/v1", }, { name: "Base URL with v2 version", apiUrl: "https://api.openai-sb.com/v2/completions", expected: "https://api.openai-sb.com/v2", }, { name: "Base URL with no version", apiUrl: "https://api.openai-sb.com/chat/completions", expected: "https://api.openai-sb.com/chat", }, { name: "Base URL with different host", apiUrl: "https://example.com/v1/chat/completions", expected: "https://example.com/v1", }, { name: "Base URL with different host", apiUrl: "https://docs-test-001.openai.azure.com/", expected: "https://docs-test-001.openai.azure.com/", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actual, _ := getModelBaseUrl(tc.apiUrl) if actual != tc.expected { t.Errorf("Expected base URL '%s', but got '%s'", tc.expected, actual) } }) } } ================================================ FILE: api/pre-commit.sh ================================================ #!/bin/bash echo "Running 'go fmt' check..." files=$(git diff --cached --name-only --diff-filter=ACM "*.go") if [ -z "$files" ]; then echo "No Go files to check." exit 0 fi unformatted_files="" for file in ${files}; do if [[ ! -z $(go fmt ${file}) ]]; then unformatted_files="$unformatted_files ${file}" fi done if [ ! -z "$unformatted_files" ]; then echo "The following files are not properly formatted:" echo "$unformatted_files" echo "Please run 'go fmt' before committing." exit 1 fi echo "'go fmt' check passed." exit 0 ================================================ FILE: api/sqlc/README.txt ================================================ you are a golang code assistant. Given table DDL, you will write all queies for sqlc in a crud applicaiton, please do not send me any generated go code. ### input CREATE TABLE chat_message ( id integer PRIMARY KEY, chat_session_id integer NOT NULL, role character varying(255) NOT NULL, content character varying NOT NULL, score double precision NOT NULL, user_id integer NOT NULL, created_at timestamp without time zone, updated_at timestamp without time zone, created_by integer NOT NULL, updated_by integer NOT NULL, raw jsonb ); ## output -- name: ListChatMessages :many SELECT * FROM chat_message ORDER BY id; -- name: ChatMessagesBySessionID :many SELECT * FROM chat_message WHERE chat_session_id = $1 ORDER BY id; -- name: ChatMessageByID :one SELECT * FROM chat_message WHERE id = $1; -- name: CreateChatMessage :one INSERT INTO chat_message (chat_session_id, role, content, model, score, user_id, created_at, updated_at, created_by, updated_by, raw) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *; -- name: UpdateChatMessage :one UPDATE chat_message SET role = $2, content = $3, score = $4, user_id = $5, updated_at = $6, updated_by = $7, raw = $8 WHERE id = $1 RETURNING *; -- name: DeleteChatMessage :exec DELETE FROM chat_message WHERE id = $1; ### input CREATE TABLE chat_session ( id integer PRIMARY KEY, user_id integer NOT NULL, topic character varying(255) NOT NULL, created_at timestamp without time zone DEFAULT now() NOT NULL, updated_at timestamp without time zone DEFAULT now() NOT NULL, active boolean default true NOT NULL, max_length integer DEFAULT 0 NOT NULL ); ### type ChatSessionService struct { q *sqlc_queries.Queries } // NewChatSessionService creates a new ChatSessionService. func NewChatSessionService(q *sqlc_queries.Queries) *ChatSessionService { return &ChatSessionService{q: q} } // CreateChatSession creates a new chat session. func (s *ChatSessionService) CreateChatSession(ctx context.Context, session_params sqlc_queries.CreateChatSessionParams) (sqlc_queries.ChatSession, error) { session, err := s.q.CreateChatSession(ctx, session_params) if err != nil { return sqlc_queries.ChatSession{}, errors.New("failed to create session") } return session, nil } // GetChatSessionByID returns a chat session by ID. func (s *ChatSessionService) GetChatSessionByID(ctx context.Context, id int32) (sqlc_queries.ChatSession, error) { session, err := s.q.GetChatSessionByID(ctx, id) if err != nil { return sqlc_queries.ChatSession{}, errors.New("failed to retrieve session") } return session, nil } // UpdateChatSession updates an existing chat session. func (s *ChatSessionService) UpdateChatSession(ctx context.Context, session_params sqlc_queries.UpdateChatSessionParams) (sqlc_queries.ChatSession, error) { session_u, err := s.q.UpdateChatSession(ctx, session_params) if err != nil { return sqlc_queries.ChatSession{}, errors.New("failed to update session") } return session_u, nil } // DeleteChatSession deletes a chat session by ID. func (s *ChatSessionService) DeleteChatSession(ctx context.Context, id int32) error { err := s.q.DeleteChatSession(ctx, id) if err != nil { return errors.New("failed to delete session") } return nil } // GetAllChatSessions returns all chat sessions. func (s *ChatSessionService) GetAllChatSessions(ctx context.Context) ([]sqlc_queries.ChatSession, error) { sessions, err := s.q.GetAllChatSessions(ctx) if err != nil { return nil, errors.New("failed to retrieve sessions") } return sessions, nil } create sql INSERT INTO auth_user (id, username, email, password, first_name, last_name, is_active, is_staff, is_superuser, date_joined) VALUES (1, 'echowuhao', 'echowuhao@gmail.com', 'pbkdf2_sha256$150000$wVq3kpPZc7pJ$+dO5tCzI9Xu9iGkWtL/Ho11DQsoOx2ZB1OVDGOlKyk4=', 'Hao', 'Wu', true, false, false, now()); Note that when generating password hashes using Django or any other library, it is important to use a strong, one-way hashing algorithm with a sufficiently high cost parameter. In this example, the cost factor is set to 150000, which should provide adequate security against brute-force attacks. DROP FUNCTION IF EXISTS tsvector_immutable(text); -- why this is necessary? CREATE FUNCTION tsvector_immutable(text) RETURNS tsvector AS $$ SELECT to_tsvector($1) $$ LANGUAGE sql IMMUTABLE; UPDATE chat_snapshot SET text = array_to_string(ARRAY(SELECT jsonb_array_elements(conversation)->>'text'), ' ')::text WHERE text = '' ALTER TABLE chat_snapshot ADD COLUMN IF NOT EXISTS text_vector tsvector generated always as ( to_tsvector(text) ) stored; ================================================ FILE: api/sqlc/queries/auth_user.sql ================================================ -- name: GetAllAuthUsers :many SELECT * FROM auth_user ORDER BY id; -- name: ListAuthUsers :many SELECT * FROM auth_user ORDER BY id LIMIT $1 OFFSET $2; -- name: GetAuthUserByID :one SELECT * FROM auth_user WHERE id = $1; -- name: GetAuthUserByEmail :one SELECT * FROM auth_user WHERE email = $1; -- name: CreateAuthUser :one INSERT INTO auth_user (email, "password", first_name, last_name, username, is_staff, is_superuser) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; -- name: UpdateAuthUser :one UPDATE auth_user SET first_name = $2, last_name= $3, last_login = now() WHERE id = $1 RETURNING first_name, last_name, email; -- name: UpdateAuthUserByEmail :one UPDATE auth_user SET first_name = $2, last_name= $3, last_login = now() WHERE email = $1 RETURNING first_name, last_name, email; -- name: DeleteAuthUser :exec DELETE FROM auth_user WHERE email = $1; -- name: GetUserByEmail :one SELECT * FROM auth_user WHERE email = $1; -- name: UpdateUserPassword :exec UPDATE auth_user SET "password" = $2 WHERE email = $1; -- name: GetTotalActiveUserCount :one SELECT COUNT(*) FROM auth_user WHERE is_active = true; -- name: UpdateAuthUserRateLimitByEmail :one INSERT INTO auth_user_management (user_id, rate_limit, created_at, updated_at) VALUES ((SELECT id FROM auth_user WHERE email = $1), $2, NOW(), NOW()) ON CONFLICT (user_id) DO UPDATE SET rate_limit = $2, updated_at = NOW() RETURNING rate_limit; -- name: GetUserStats :many SELECT auth_user.first_name, auth_user.last_name, auth_user.email AS user_email, COALESCE(user_stats.total_messages, 0) AS total_chat_messages, COALESCE(user_stats.total_token_count, 0) AS total_token_count, COALESCE(user_stats.total_messages_3_days, 0) AS total_chat_messages_3_days, COALESCE(user_stats.total_token_count_3_days, 0) AS total_token_count_3_days, COALESCE(auth_user_management.rate_limit, @default_rate_limit::INTEGER) AS rate_limit FROM auth_user LEFT JOIN ( SELECT chat_message_stats.user_id, SUM(total_messages) AS total_messages, SUM(total_token_count) AS total_token_count, SUM(CASE WHEN created_at >= NOW() - INTERVAL '3 days' THEN total_messages ELSE 0 END) AS total_messages_3_days, SUM(CASE WHEN created_at >= NOW() - INTERVAL '3 days' THEN total_token_count ELSE 0 END) AS total_token_count_3_days FROM ( SELECT user_id, COUNT(*) AS total_messages, SUM(token_count) as total_token_count, MAX(created_at) AS created_at FROM chat_message GROUP BY user_id, chat_session_uuid ) AS chat_message_stats GROUP BY chat_message_stats.user_id ) AS user_stats ON auth_user.id = user_stats.user_id LEFT JOIN auth_user_management ON auth_user.id = auth_user_management.user_id ORDER BY total_chat_messages DESC, auth_user.id DESC OFFSET $2 LIMIT $1; -- name: GetUserAnalysisByEmail :one SELECT auth_user.first_name, auth_user.last_name, auth_user.email AS user_email, COALESCE(user_stats.total_messages, 0) AS total_messages, COALESCE(user_stats.total_token_count, 0) AS total_tokens, COALESCE(user_stats.total_sessions, 0) AS total_sessions, COALESCE(user_stats.total_messages_3_days, 0) AS messages_3_days, COALESCE(user_stats.total_token_count_3_days, 0) AS tokens_3_days, COALESCE(auth_user_management.rate_limit, @default_rate_limit::INTEGER) AS rate_limit FROM auth_user LEFT JOIN ( SELECT stats.user_id, SUM(stats.total_messages) AS total_messages, SUM(stats.total_token_count) AS total_token_count, COUNT(DISTINCT stats.chat_session_uuid) AS total_sessions, SUM(CASE WHEN stats.created_at >= NOW() - INTERVAL '3 days' THEN stats.total_messages ELSE 0 END) AS total_messages_3_days, SUM(CASE WHEN stats.created_at >= NOW() - INTERVAL '3 days' THEN stats.total_token_count ELSE 0 END) AS total_token_count_3_days FROM ( SELECT user_id, chat_session_uuid, COUNT(*) AS total_messages, SUM(token_count) as total_token_count, MAX(created_at) AS created_at FROM chat_message WHERE is_deleted = false GROUP BY user_id, chat_session_uuid ) AS stats GROUP BY stats.user_id ) AS user_stats ON auth_user.id = user_stats.user_id LEFT JOIN auth_user_management ON auth_user.id = auth_user_management.user_id WHERE auth_user.email = $1; -- name: GetUserModelUsageByEmail :many SELECT COALESCE(cm.model, 'unknown') AS model, COUNT(*) AS message_count, COALESCE(SUM(cm.token_count), 0) AS token_count, MAX(cm.created_at)::timestamp AS last_used FROM chat_message cm INNER JOIN auth_user au ON cm.user_id = au.id WHERE au.email = $1 AND cm.is_deleted = false AND cm.role = 'assistant' AND cm.model IS NOT NULL AND cm.model != '' GROUP BY cm.model ORDER BY message_count DESC; -- name: GetUserRecentActivityByEmail :many SELECT DATE(cm.created_at) AS activity_date, COUNT(*) AS messages, COALESCE(SUM(cm.token_count), 0) AS tokens, COUNT(DISTINCT cm.chat_session_uuid) AS sessions FROM chat_message cm INNER JOIN auth_user au ON cm.user_id = au.id WHERE au.email = $1 AND cm.is_deleted = false AND cm.created_at >= NOW() - INTERVAL '30 days' GROUP BY DATE(cm.created_at) ORDER BY activity_date DESC LIMIT 30; -- name: GetUserSessionHistoryByEmail :many SELECT cs.uuid AS session_id, cs.model, COALESCE(COUNT(cm.id), 0) AS message_count, COALESCE(SUM(cm.token_count), 0) AS token_count, COALESCE(MIN(cm.created_at), cs.created_at)::timestamp AS created_at, COALESCE(MAX(cm.created_at), cs.updated_at)::timestamp AS updated_at FROM chat_session cs INNER JOIN auth_user au ON cs.user_id = au.id LEFT JOIN chat_message cm ON cs.uuid = cm.chat_session_uuid AND cm.is_deleted = false WHERE au.email = $1 AND cs.active = true GROUP BY cs.uuid, cs.model, cs.created_at, cs.updated_at ORDER BY cs.updated_at DESC LIMIT $2 OFFSET $3; -- name: GetUserSessionHistoryCountByEmail :one SELECT COUNT(DISTINCT cs.uuid) AS total_sessions FROM chat_session cs INNER JOIN auth_user au ON cs.user_id = au.id WHERE au.email = $1 AND cs.active = true; ================================================ FILE: api/sqlc/queries/auth_user_management.sql ================================================ -- name: GetRateLimit :one -- GetRateLimit retrieves the rate limit for a user from the auth_user_management table. -- If no rate limit is set for the user, it returns the default rate limit of 100. SELECT rate_limit AS rate_limit FROM auth_user_management WHERE user_id = $1; ================================================ FILE: api/sqlc/queries/bot_answer_history.sql ================================================ -- Bot Answer History Queries -- -- name: CreateBotAnswerHistory :one INSERT INTO bot_answer_history ( bot_uuid, user_id, prompt, answer, model, tokens_used ) VALUES ( $1, $2, $3, $4, $5, $6 ) RETURNING *; -- name: GetBotAnswerHistoryByID :one SELECT bah.id, bah.bot_uuid, bah.user_id, bah.prompt, bah.answer, bah.model, bah.tokens_used, bah.created_at, bah.updated_at, au.username AS user_username, au.email AS user_email FROM bot_answer_history bah JOIN auth_user au ON bah.user_id = au.id WHERE bah.id = $1; -- name: GetBotAnswerHistoryByBotUUID :many SELECT bah.id, bah.bot_uuid, bah.user_id, bah.prompt, bah.answer, bah.model, bah.tokens_used, bah.created_at, bah.updated_at, au.username AS user_username, au.email AS user_email FROM bot_answer_history bah JOIN auth_user au ON bah.user_id = au.id WHERE bah.bot_uuid = $1 ORDER BY bah.created_at DESC LIMIT $2 OFFSET $3; -- name: GetBotAnswerHistoryByUserID :many SELECT bah.id, bah.bot_uuid, bah.user_id, bah.prompt, bah.answer, bah.model, bah.tokens_used, bah.created_at, bah.updated_at, au.username AS user_username, au.email AS user_email FROM bot_answer_history bah JOIN auth_user au ON bah.user_id = au.id WHERE bah.user_id = $1 ORDER BY bah.created_at DESC LIMIT $2 OFFSET $3; -- name: UpdateBotAnswerHistory :one UPDATE bot_answer_history SET answer = $2, tokens_used = $3, updated_at = NOW() WHERE id = $1 RETURNING *; -- name: DeleteBotAnswerHistory :exec DELETE FROM bot_answer_history WHERE id = $1; -- name: GetBotAnswerHistoryCountByBotUUID :one SELECT COUNT(*) FROM bot_answer_history WHERE bot_uuid = $1; -- name: GetBotAnswerHistoryCountByUserID :one SELECT COUNT(*) FROM bot_answer_history WHERE user_id = $1; -- name: GetLatestBotAnswerHistoryByBotUUID :many SELECT bah.id, bah.bot_uuid, bah.user_id, bah.prompt, bah.answer, bah.model, bah.tokens_used, bah.created_at, bah.updated_at, au.username AS user_username, au.email AS user_email FROM bot_answer_history bah JOIN auth_user au ON bah.user_id = au.id WHERE bah.bot_uuid = $1 ORDER BY bah.created_at DESC LIMIT $2; ================================================ FILE: api/sqlc/queries/chat_comment.sql ================================================ -- name: CreateChatComment :one INSERT INTO chat_comment ( uuid, chat_session_uuid, chat_message_uuid, content, created_by, updated_by ) VALUES ( $1, $2, $3, $4, $5, $5 ) RETURNING *; -- name: GetCommentsBySessionUUID :many SELECT cc.uuid, cc.chat_message_uuid, cc.content, cc.created_at, au.username AS author_username, au.email AS author_email FROM chat_comment cc JOIN auth_user au ON cc.created_by = au.id WHERE cc.chat_session_uuid = $1 ORDER BY cc.created_at DESC; -- name: GetCommentsByMessageUUID :many SELECT cc.uuid, cc.content, cc.created_at, au.username AS author_username, au.email AS author_email FROM chat_comment cc JOIN auth_user au ON cc.created_by = au.id WHERE cc.chat_message_uuid = $1 ORDER BY cc.created_at DESC; ================================================ FILE: api/sqlc/queries/chat_file.sql ================================================ -- name: CreateChatFile :one INSERT INTO chat_file (name, data, user_id, chat_session_uuid, mime_type) VALUES ($1, $2, $3, $4, $5) RETURNING *; -- name: ListChatFilesBySessionUUID :many SELECT id, name FROM chat_file WHERE user_id = $1 and chat_session_uuid = $2 ORDER BY created_at ; -- name: ListChatFilesWithContentBySessionUUID :many SELECT * FROM chat_file WHERE chat_session_uuid = $1 ORDER BY created_at; -- name: GetChatFileByID :one SELECT id, name, data, created_at, user_id, chat_session_uuid FROM chat_file WHERE id = $1; -- name: DeleteChatFile :one DELETE FROM chat_file WHERE id = $1 RETURNING *; ================================================ FILE: api/sqlc/queries/chat_log.sql ================================================ -- name: ListChatLogs :many SELECT * FROM chat_logs ORDER BY id; -- name: ChatLogByID :one SELECT * FROM chat_logs WHERE id = $1; -- name: CreateChatLog :one INSERT INTO chat_logs (session, question, answer) VALUES ($1, $2, $3) RETURNING *; -- name: UpdateChatLog :one UPDATE chat_logs SET session = $2, question = $3, answer = $4 WHERE id = $1 RETURNING *; -- name: DeleteChatLog :exec DELETE FROM chat_logs WHERE id = $1; ================================================ FILE: api/sqlc/queries/chat_message.sql ================================================ -- name: GetAllChatMessages :many SELECT * FROM chat_message WHERE is_deleted = false ORDER BY id; -- name: GetChatMessagesBySessionUUID :many SELECT cm.* FROM chat_message cm INNER JOIN chat_session cs ON cm.chat_session_uuid = cs.uuid WHERE cm.is_deleted = false and cs.active = true and cs.uuid = $1 ORDER BY cm.id OFFSET $2 LIMIT $3; -- name: GetChatMessageBySessionUUID :one SELECT cm.* FROM chat_message cm INNER JOIN chat_session cs ON cm.chat_session_uuid = cs.uuid WHERE cm.is_deleted = false and cs.active = true and cs.uuid = $1 ORDER BY cm.id OFFSET $2 LIMIT $1; -- name: GetChatMessageByID :one SELECT * FROM chat_message WHERE is_deleted = false and id = $1; -- name: CreateChatMessage :one INSERT INTO chat_message (chat_session_uuid, uuid, role, content, reasoning_content, model, token_count, score, user_id, created_by, updated_by, llm_summary, raw, artifacts, suggested_questions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *; -- name: UpdateChatMessage :one UPDATE chat_message SET role = $2, content = $3, score = $4, user_id = $5, updated_by = $6, artifacts = $7, suggested_questions = $8, updated_at = now() WHERE id = $1 RETURNING *; -- name: DeleteChatMessage :exec UPDATE chat_message set is_deleted = true, updated_at = now() WHERE id = $1; ---- UUID ---- -- name: GetChatMessageByUUID :one SELECT * FROM chat_message WHERE is_deleted = false and uuid = $1; -- name: UpdateChatMessageByUUID :one UPDATE chat_message SET content = $2, is_pin = $3, token_count = $4, artifacts = $5, suggested_questions = $6, updated_at = now() WHERE uuid = $1 RETURNING *; -- name: DeleteChatMessageByUUID :exec UPDATE chat_message SET is_deleted = true, updated_at = now() WHERE uuid = $1; -- name: HasChatMessagePermission :one SELECT COUNT(*) > 0 as has_permission FROM chat_message cm INNER JOIN chat_session cs ON cm.chat_session_uuid = cs.uuid INNER JOIN auth_user au ON cs.user_id = au.id WHERE cm.is_deleted = false and cm.id = $1 AND (cs.user_id = $2 OR au.is_superuser) and cs.active = true; -- name: GetLatestMessagesBySessionUUID :many SELECT * FROM chat_message Where chat_message.id in ( SELECT chat_message.id FROM chat_message WHERE chat_message.chat_session_uuid = $1 and chat_message.is_deleted = false and chat_message.is_pin = true UNION ( SELECT chat_message.id FROM chat_message WHERE chat_message.chat_session_uuid = $1 and chat_message.is_deleted = false -- and chat_message.is_pin = false ORDER BY created_at DESC LIMIT $2 ) ) ORDER BY created_at; -- name: GetFirstMessageBySessionUUID :one SELECT * FROM chat_message WHERE chat_session_uuid = $1 and is_deleted = false ORDER BY created_at LIMIT 1; -- name: GetLastNChatMessages :many SELECT * FROM chat_message WHERE chat_message.id in ( SELECT id FROM chat_message cm WHERE cm.chat_session_uuid = $3 and cm.is_deleted = false and cm.is_pin = true UNION ( SELECT id FROM chat_message cm WHERE cm.chat_session_uuid = $3 AND cm.id < (SELECT id FROM chat_message WHERE chat_message.uuid = $1) AND cm.is_deleted = false -- and cm.is_pin = false ORDER BY cm.created_at DESC LIMIT $2 ) ) ORDER BY created_at; -- name: UpdateChatMessageContent :exec 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 SET is_deleted = true, updated_at = now() WHERE is_deleted = false and is_pin = false and chat_session_uuid = $1; -- name: GetChatMessagesCount :one -- Get total chat message count for user in last 10 minutes SELECT COUNT(*) FROM chat_message WHERE user_id = $1 AND created_at >= NOW() - INTERVAL '10 minutes'; -- name: GetChatMessagesCountByUserAndModel :one -- Get total chat message count for user of model in last 10 minutes SELECT COUNT(*) FROM chat_message cm JOIN chat_session cs ON (cm.chat_session_uuid = cs.uuid AND cs.user_id = cm.user_id) WHERE cm.user_id = $1 AND cs.model = $2 AND cm.created_at >= NOW() - INTERVAL '10 minutes'; -- name: GetLatestUsageTimeOfModel :many SELECT model, MAX(created_at)::timestamp as latest_message_time, COUNT(*) as message_count FROM chat_message WHERE created_at >= NOW() - sqlc.arg(time_interval)::text::INTERVAL AND is_deleted = false AND model != '' AND role = 'assistant' GROUP BY model ORDER BY latest_message_time DESC; -- name: GetChatMessagesBySessionUUIDForAdmin :many SELECT id, uuid, role, content, reasoning_content, model, token_count, user_id, created_at, updated_at FROM ( -- Include session prompts as the first messages SELECT cp.id, cp.uuid, cp.role, cp.content, ''::text as reasoning_content, cs.model, cp.token_count, cp.user_id, cp.created_at, cp.updated_at FROM chat_prompt cp INNER JOIN chat_session cs ON cp.chat_session_uuid = cs.uuid WHERE cp.chat_session_uuid = $1 AND cp.is_deleted = false AND cp.role = 'system' UNION ALL -- Include regular chat messages SELECT id, uuid, role, content, reasoning_content, model, token_count, user_id, created_at, updated_at FROM chat_message WHERE chat_session_uuid = $1 AND is_deleted = false ) combined_messages ORDER BY created_at ASC; ================================================ FILE: api/sqlc/queries/chat_model.sql ================================================ -- name: ListChatModels :many SELECT * FROM chat_model ORDER BY order_number; -- name: ListSystemChatModels :many SELECT * FROM chat_model where user_id in (select id from auth_user where is_superuser = true) ORDER BY order_number, id desc; -- name: ChatModelByID :one SELECT * FROM chat_model WHERE id = $1; -- name: ChatModelByName :one SELECT * FROM chat_model WHERE name = $1; -- name: CreateChatModel :one INSERT INTO chat_model (name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, api_type ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *; -- name: UpdateChatModel :one UPDATE chat_model SET name = $2, label = $3, is_default = $4, url = $5, api_auth_header = $6, api_auth_key = $7, enable_per_mode_ratelimit = $9, max_token = $10, default_token = $11, order_number = $12, http_time_out = $13, is_enable = $14, api_type = $15 WHERE id = $1 and user_id = $8 RETURNING *; -- name: UpdateChatModelKey :one UPDATE chat_model SET api_auth_key = $2 WHERE id = $1 RETURNING *; -- name: DeleteChatModel :exec DELETE FROM chat_model WHERE id = $1 and user_id = $2; -- name: GetDefaultChatModel :one SELECT * FROM chat_model WHERE is_default = true and user_id in (select id from auth_user where is_superuser = true) ORDER BY order_number, id LIMIT 1; ================================================ FILE: api/sqlc/queries/chat_prompt.sql ================================================ -- name: GetAllChatPrompts :many SELECT * FROM chat_prompt WHERE is_deleted = false ORDER BY id; -- name: GetChatPromptByID :one SELECT * FROM chat_prompt WHERE is_deleted = false and id = $1; -- name: CreateChatPrompt :one INSERT INTO chat_prompt (uuid, chat_session_uuid, role, content, token_count, user_id, created_by, updated_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; -- name: UpdateChatPrompt :one UPDATE chat_prompt SET chat_session_uuid = $2, role = $3, content = $4, score = $5, user_id = $6, updated_at = now(), updated_by = $7 WHERE id = $1 RETURNING *; -- name: UpdateChatPromptByUUID :one UPDATE chat_prompt SET content = $2, token_count = $3, updated_at = now() WHERE uuid = $1 and is_deleted = false RETURNING *; -- name: DeleteChatPrompt :exec UPDATE chat_prompt SET is_deleted = true, updated_at = now() WHERE id = $1; -- name: GetChatPromptsByUserID :many SELECT * FROM chat_prompt WHERE user_id = $1 and is_deleted = false ORDER BY id; -- name: GetChatPromptsBysession_uuid :many SELECT * FROM chat_prompt WHERE chat_session_uuid = $1 and is_deleted = false ORDER BY id; -- name: GetChatPromptsBySessionUUID :many SELECT * FROM chat_prompt WHERE chat_session_uuid = $1 and is_deleted = false ORDER BY id; -- name: GetOneChatPromptBySessionUUID :one SELECT * FROM chat_prompt WHERE chat_session_uuid = $1 and is_deleted = false ORDER BY id LIMIT 1; -- name: HasChatPromptPermission :one SELECT COUNT(*) > 0 as has_permission FROM chat_prompt cp INNER JOIN auth_user au ON cp.user_id = au.id WHERE cp.id = $1 AND (cp.user_id = $2 OR au.is_superuser) AND cp.is_deleted = false; -- name: DeleteChatPromptByUUID :exec UPDATE chat_prompt SET is_deleted = true, updated_at = now() WHERE uuid = $1; -- name: GetChatPromptByUUID :one SELECT * FROM chat_prompt WHERE uuid = $1; ================================================ FILE: api/sqlc/queries/chat_session.sql ================================================ -- name: GetAllChatSessions :many SELECT * FROM chat_session where active = true ORDER BY id; -- name: CreateChatSession :one INSERT INTO chat_session (user_id, topic, max_length, uuid, model) VALUES ($1, $2, $3, $4, $5) RETURNING *; -- name: UpdateChatSession :one UPDATE chat_session SET user_id = $2, topic = $3, updated_at = now(), active = $4 WHERE id = $1 RETURNING *; -- name: DeleteChatSession :exec DELETE FROM chat_session WHERE id = $1; -- name: GetChatSessionByID :one SELECT * FROM chat_session WHERE id = $1; -- name: GetChatSessionByUUID :one SELECT * FROM chat_session WHERE active = true and uuid = $1 order by updated_at; -- name: GetChatSessionByUUIDWithInActive :one SELECT * FROM chat_session WHERE uuid = $1 order by updated_at; -- name: CreateChatSessionByUUID :one INSERT INTO chat_session (user_id, uuid, topic, created_at, active, max_length, model) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; -- name: UpdateChatSessionByUUID :one UPDATE chat_session SET user_id = $2, topic = $3, updated_at = now() WHERE uuid = $1 RETURNING *; -- name: CreateOrUpdateChatSessionByUUID :one INSERT INTO chat_session(uuid, user_id, topic, max_length, temperature, model, max_tokens, top_p, n, debug, summarize_mode, workspace_id, explore_mode, artifact_enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (uuid) DO UPDATE SET max_length = EXCLUDED.max_length, debug = EXCLUDED.debug, max_tokens = EXCLUDED.max_tokens, temperature = EXCLUDED.temperature, top_p = EXCLUDED.top_p, n= EXCLUDED.n, model = EXCLUDED.model, summarize_mode = EXCLUDED.summarize_mode, artifact_enabled = EXCLUDED.artifact_enabled, workspace_id = CASE WHEN EXCLUDED.workspace_id IS NOT NULL THEN EXCLUDED.workspace_id ELSE chat_session.workspace_id END, topic = CASE WHEN chat_session.topic IS NULL THEN EXCLUDED.topic ELSE chat_session.topic END, explore_mode = EXCLUDED.explore_mode, updated_at = now() returning *; -- name: UpdateChatSessionTopicByUUID :one INSERT INTO chat_session(uuid, user_id, topic) VALUES ($1, $2, $3) ON CONFLICT (uuid) DO UPDATE SET topic = EXCLUDED.topic, updated_at = now() returning *; -- name: DeleteChatSessionByUUID :exec update chat_session set active = false WHERE uuid = $1 returning *; -- name: GetChatSessionsByUserID :many SELECT cs.* FROM chat_session cs LEFT JOIN ( SELECT chat_session_uuid, MAX(created_at) AS latest_message_time FROM chat_message GROUP BY chat_session_uuid ) cm ON cs.uuid = cm.chat_session_uuid WHERE cs.user_id = $1 AND cs.active = true ORDER BY cm.latest_message_time DESC, cs.id DESC; -- SELECT cs.* -- FROM chat_session cs -- WHERE cs.user_id = $1 and cs.active = true -- ORDER BY cs.updated_at DESC; -- name: HasChatSessionPermission :one SELECT COUNT(*) > 0 as has_permission FROM chat_session cs INNER JOIN auth_user au ON cs.user_id = au.id WHERE cs.id = $1 AND (cs.user_id = $2 OR au.is_superuser); -- name: UpdateSessionMaxLength :one UPDATE chat_session SET max_length = $2, updated_at = now() WHERE uuid = $1 RETURNING *; -- name: CreateChatSessionInWorkspace :one INSERT INTO chat_session (user_id, uuid, topic, created_at, active, max_length, model, workspace_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; -- name: UpdateSessionWorkspace :one UPDATE chat_session SET workspace_id = $2, updated_at = now() WHERE uuid = $1 RETURNING *; -- name: GetSessionsByWorkspaceID :many SELECT cs.* FROM chat_session cs LEFT JOIN ( SELECT chat_session_uuid, MAX(created_at) AS latest_message_time FROM chat_message GROUP BY chat_session_uuid ) cm ON cs.uuid = cm.chat_session_uuid WHERE cs.workspace_id = $1 AND cs.active = true ORDER BY cm.latest_message_time DESC, cs.id DESC; -- name: GetSessionsGroupedByWorkspace :many SELECT cs.*, w.uuid as workspace_uuid, w.name as workspace_name, w.color as workspace_color, w.icon as workspace_icon FROM chat_session cs LEFT JOIN chat_workspace w ON cs.workspace_id = w.id LEFT JOIN ( SELECT chat_session_uuid, MAX(created_at) AS latest_message_time FROM chat_message GROUP BY chat_session_uuid ) cm ON cs.uuid = cm.chat_session_uuid WHERE cs.user_id = $1 AND cs.active = true ORDER BY w.order_position ASC, cm.latest_message_time DESC, cs.id DESC; -- name: MigrateSessionsToDefaultWorkspace :exec UPDATE chat_session SET workspace_id = $2 WHERE user_id = $1 AND workspace_id IS NULL; -- name: GetSessionsWithoutWorkspace :many SELECT * FROM chat_session WHERE user_id = $1 AND workspace_id IS NULL AND active = true; ================================================ FILE: api/sqlc/queries/chat_snapshot.sql ================================================ -- name: ListChatSnapshots :many SELECT * FROM chat_snapshot ORDER BY id; -- name: ChatSnapshotByID :one SELECT * FROM chat_snapshot WHERE id = $1; -- name: CreateChatSnapshot :one INSERT INTO chat_snapshot (uuid, user_id, title, model, summary, tags, conversation ,session, text ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; -- name: CreateChatBot :one INSERT INTO chat_snapshot (uuid, user_id, typ, title, model, summary, tags, conversation ,session, text ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *; -- name: UpdateChatSnapshot :one UPDATE chat_snapshot SET uuid = $2, user_id = $3, title = $4, summary = $5, tags = $6, conversation = $7, created_at = $8 WHERE id = $1 RETURNING *; -- name: DeleteChatSnapshot :one DELETE FROM chat_snapshot WHERE uuid = $1 and user_id = $2 RETURNING *; -- name: ChatSnapshotByUUID :one SELECT * FROM chat_snapshot WHERE uuid = $1; -- name: ChatSnapshotByUserIdAndUuid :one SELECT * FROM chat_snapshot WHERE user_id = $1 AND uuid = $2; -- name: ChatSnapshotMetaByUserID :many SELECT uuid, title, summary, tags, created_at, typ FROM chat_snapshot WHERE user_id = $1 and typ = $2 order by created_at desc LIMIT $3 OFFSET $4; -- name: UpdateChatSnapshotMetaByUUID :exec UPDATE chat_snapshot SET title = $2, summary = $3 WHERE uuid = $1 and user_id = $4; -- name: ChatSnapshotCountByUserIDAndType :one SELECT COUNT(*) FROM chat_snapshot WHERE user_id = $1 AND ($2::text = '' OR typ = $2); -- name: ChatSnapshotSearch :many SELECT uuid, title, ts_rank(search_vector, websearch_to_tsquery(@search), 1) as rank FROM chat_snapshot WHERE search_vector @@ websearch_to_tsquery(@search) AND user_id = $1 ORDER BY rank DESC LIMIT 20; ================================================ FILE: api/sqlc/queries/chat_workspace.sql ================================================ -- name: CreateWorkspace :one INSERT INTO chat_workspace (uuid, user_id, name, description, color, icon, is_default, order_position) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; -- name: GetWorkspaceByUUID :one SELECT * FROM chat_workspace WHERE uuid = $1; -- name: GetWorkspacesByUserID :many SELECT * FROM chat_workspace WHERE user_id = $1 ORDER BY order_position ASC, created_at ASC; -- name: UpdateWorkspace :one UPDATE chat_workspace SET name = $2, description = $3, color = $4, icon = $5, updated_at = now() WHERE uuid = $1 RETURNING *; -- name: UpdateWorkspaceOrder :one UPDATE chat_workspace SET order_position = $2, updated_at = now() WHERE uuid = $1 RETURNING *; -- name: DeleteWorkspace :exec DELETE FROM chat_workspace WHERE uuid = $1; -- name: GetDefaultWorkspaceByUserID :one SELECT * FROM chat_workspace WHERE user_id = $1 AND is_default = true LIMIT 1; -- name: SetDefaultWorkspace :one UPDATE chat_workspace SET is_default = $2, updated_at = now() WHERE uuid = $1 RETURNING *; -- name: GetWorkspaceWithSessionCount :many SELECT w.*, COUNT(cs.id) as session_count FROM chat_workspace w LEFT JOIN chat_session cs ON w.id = cs.workspace_id AND cs.active = true WHERE w.user_id = $1 GROUP BY w.id ORDER BY w.order_position ASC, w.created_at ASC; -- name: CreateDefaultWorkspace :one INSERT INTO chat_workspace (uuid, user_id, name, description, color, icon, is_default, order_position) VALUES ($1, $2, 'General', 'Default workspace for all conversations', '#6366f1', 'folder', true, 0) RETURNING *; -- name: HasWorkspacePermission :one SELECT COUNT(*) > 0 as has_permission FROM chat_workspace w WHERE w.uuid = $1 AND ( w.user_id = $2 OR EXISTS ( SELECT 1 FROM auth_user request_user WHERE request_user.id = $2 AND request_user.is_superuser = true ) ); ================================================ FILE: api/sqlc/queries/jwt_secrets.sql ================================================ -- name: CreateJwtSecret :one INSERT INTO jwt_secrets (name, secret, audience) VALUES ($1, $2, $3) RETURNING *; -- name: GetJwtSecret :one SELECT * FROM jwt_secrets WHERE name = $1; -- name: DeleteAllJwtSecrets :execrows DELETE FROM jwt_secrets; ================================================ FILE: api/sqlc/queries/user_active_chat_session.sql ================================================ -- Simplified unified queries for active sessions -- name: UpsertUserActiveSession :one INSERT INTO user_active_chat_session (user_id, workspace_id, chat_session_uuid) VALUES ($1, $2, $3) ON CONFLICT (user_id, COALESCE(workspace_id, -1)) DO UPDATE SET chat_session_uuid = EXCLUDED.chat_session_uuid, updated_at = now() RETURNING *; -- name: GetUserActiveSession :one SELECT * FROM user_active_chat_session WHERE user_id = $1 AND ( (workspace_id IS NULL AND $2::int IS NULL) OR (workspace_id = $2) ); -- name: GetAllUserActiveSessions :many SELECT * FROM user_active_chat_session WHERE user_id = $1 ORDER BY workspace_id NULLS FIRST, updated_at DESC; -- name: DeleteUserActiveSession :exec DELETE FROM user_active_chat_session WHERE user_id = $1 AND ( (workspace_id IS NULL AND $2::int IS NULL) OR (workspace_id = $2) ); -- name: DeleteUserActiveSessionBySession :exec DELETE FROM user_active_chat_session WHERE user_id = $1 AND chat_session_uuid = $2; ================================================ FILE: api/sqlc/queries/user_chat_model_privilege.sql ================================================ -- name: ListUserChatModelPrivileges :many SELECT * FROM user_chat_model_privilege ORDER BY id; -- name: ListUserChatModelPrivilegesRateLimit :many SELECT ucmp.id, au.email as user_email, CONCAT_WS('',au.last_name, au.first_name) as full_name, cm.name chat_model_name, ucmp.rate_limit FROM user_chat_model_privilege ucmp INNER JOIN chat_model cm ON cm.id = ucmp.chat_model_id INNER JOIN auth_user au ON au.id = ucmp.user_id ORDER by au.last_login DESC; -- TODO add ratelimit -- LIMIT 1000 -- name: ListUserChatModelPrivilegesByUserID :many SELECT * FROM user_chat_model_privilege WHERE user_id = $1 ORDER BY id; -- name: UserChatModelPrivilegeByID :one SELECT * FROM user_chat_model_privilege WHERE id = $1; -- name: CreateUserChatModelPrivilege :one INSERT INTO user_chat_model_privilege (user_id, chat_model_id, rate_limit, created_by, updated_by) VALUES ($1, $2, $3, $4, $5) RETURNING *; -- name: UpdateUserChatModelPrivilege :one UPDATE user_chat_model_privilege SET rate_limit = $2, updated_at = now(), updated_by = $3 WHERE id = $1 RETURNING *; -- name: DeleteUserChatModelPrivilege :exec DELETE FROM user_chat_model_privilege WHERE id = $1; -- name: UserChatModelPrivilegeByUserAndModelID :one SELECT * FROM user_chat_model_privilege WHERE user_id = $1 AND chat_model_id = $2; -- name: RateLimiteByUserAndSessionUUID :one SELECT ucmp.rate_limit, cm.name AS chat_model_name FROM user_chat_model_privilege ucmp JOIN chat_session cs ON cs.user_id = ucmp.user_id JOIN chat_model cm ON (cm.id = ucmp.chat_model_id AND cs.model = cm.name and cm.enable_per_mode_ratelimit = true) WHERE cs.uuid = $1 AND ucmp.user_id = $2; -- AND cs.model = cm.name ================================================ FILE: api/sqlc/schema.sql ================================================ CREATE TABLE IF NOT EXISTS jwt_secrets ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, secret TEXT NOT NULL, audience TEXT NOT NULL, lifetime smallint NOT NULL default 24 ); ALTER TABLE jwt_secrets ADD COLUMN IF NOT EXISTS lifetime smallint NOT NULL default 24; UPDATE jwt_secrets SET lifetime = 240; CREATE TABLE IF NOT EXISTS chat_model ( id SERIAL PRIMARY KEY, -- model name 'claude-v1', 'gpt-3.5-turbo' name TEXT UNIQUE DEFAULT '' NOT NULL, -- model label 'Claude', 'GPT-3.5 Turbo' label TEXT DEFAULT '' NOT NULL, is_default BOOLEAN DEFAULT false NOT NULL, url TEXT DEFAULT '' NOT NULL, api_auth_header TEXT DEFAULT '' NOT NULL, -- env var that contains the api key -- for example: OPENAI_API_KEY, which means the api key is stored in an env var called OPENAI_API_KEY api_auth_key TEXT DEFAULT '' NOT NULL, user_id INTEGER NOT NULL default 1, enable_per_mode_ratelimit BOOLEAN DEFAULT false NOT NULL, max_token INTEGER NOT NULL default 120, default_token INTEGER NOT NULL default 120, order_number INTEGER NOT NULL default 1, http_time_out INTEGER NOT NULL default 120 ); ALTER TABLE chat_model ADD COLUMN IF NOT EXISTS user_id INTEGER NOT NULL default 1; ALTER TABLE chat_model ADD COLUMN IF NOT EXISTS enable_per_mode_ratelimit BOOLEAN DEFAULT false NOT NULL; ALTER TABLE chat_model ADD COLUMN IF NOT EXISTS max_token INTEGER NOT NULL default 4096; ALTER TABLE chat_model ADD COLUMN IF NOT EXISTS default_token INTEGER NOT NULL default 2048; ALTER TABLE chat_model ADD COLUMN IF NOT EXISTS order_number INTEGER NOT NULL default 1; ALTER TABLE chat_model ADD COLUMN IF NOT EXISTS http_time_out INTEGER NOT NULL default 120; ALTER TABLE chat_model ADD COLUMN IF NOT EXISTS is_enable BOOLEAN DEFAULT true NOT NULL; ALTER TABLE chat_model ADD COLUMN IF NOT EXISTS api_type VARCHAR(50) NOT NULL DEFAULT 'openai'; INSERT INTO chat_model(name, label, is_default, url, api_auth_header, api_auth_key, max_token, default_token, order_number) VALUES ('gpt-3.5-turbo', 'gpt-3.5-turbo(chatgpt)', true, 'https://api.openai.com/v1/chat/completions', 'Authorization', 'OPENAI_API_KEY', 4096, 4096, 0), ('gemini-2.0-flash', 'gemini-2.0-flash', false, 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash', 'Authorization', 'GEMINI_API_KEY', 4096, 4096, 0), ('gpt-3.5-turbo-16k', 'gpt-3.5-16k', false, 'https://api.openai.com/v1/chat/completions', 'Authorization', 'OPENAI_API_KEY', 16384, 8192, 2), ('claude-3-7-sonnet-20250219', 'claude-3-7-sonnet-20250219', false, 'https://api.anthropic.com/v1/messages', 'x-api-key', 'CLAUDE_API_KEY', 4096, 4096, 4), ('gpt-4', 'gpt-4(chatgpt)', false, 'https://api.openai.com/v1/chat/completions', 'Authorization', 'OPENAI_API_KEY', 9192, 4096, 5), ('deepseek-chat', 'deepseek-chat', false, 'https://api.deepseek.com/v1/chat/completions', 'Authorization', 'DEEPSEEK_API_KEY', 8192, 8192, 0), ('gpt-4-32k', 'gpt-4-32k(chatgpt)', false, 'https://api.openai.com/v1/chat/completions', 'Authorization', 'OPENAI_API_KEY', 9192, 2048, 6), ('text-davinci-003', 'text-davinci-003', false, 'https://api.openai.com/v1/completions', 'Authorization', 'OPENAI_API_KEY', 4096, 2048, 7), ('echo','echo',false,'https://bestqa_workerd.bestqa.workers.dev/echo','Authorization','ECHO_API_KEY', 40960, 20480, 8), ('debug','debug',false,'https://bestqa_workerd.bestqa.workers.dev/debug','Authorization','ECHO_API_KEY', 40960, 2048, 9), ('deepseek-reasoner','deepseek-reasoner',false,'https://api.deepseek.com/v1/chat/completions','Authorization','DEEPSEEK API KEY', 8192, 8192, 2) ON CONFLICT(name) DO NOTHING; UPDATE chat_model SET enable_per_mode_ratelimit = true WHERE name = 'gpt-4'; UPDATE chat_model SET enable_per_mode_ratelimit = true WHERE name = 'gpt-4-32k'; DELETE FROM chat_model where name = 'claude-v1'; DELETE FROM chat_model where name = 'claude-v1-100k'; DELETE FROM chat_model where name = 'claude-instant-v1'; -- Update existing records with appropriate api_type values UPDATE chat_model SET api_type = 'openai' WHERE name LIKE 'gpt-%' OR name LIKE 'text-davinci-%' OR name LIKE 'deepseek-%'; UPDATE chat_model SET api_type = 'claude' WHERE name LIKE 'claude-%'; UPDATE chat_model SET api_type = 'gemini' WHERE name LIKE 'gemini-%'; UPDATE chat_model SET api_type = 'ollama' WHERE name LIKE 'ollama-%'; UPDATE chat_model SET api_type = 'custom' WHERE name LIKE 'custom-%' OR name IN ('echo', 'debug'); -- create index on name CREATE INDEX IF NOT EXISTS jwt_secrets_name_idx ON jwt_secrets (name); CREATE TABLE IF NOT EXISTS auth_user ( id SERIAL PRIMARY KEY, password VARCHAR(128) NOT NULL, last_login TIMESTAMP default now() NOT NULL, is_superuser BOOLEAN default false NOT NULL, username VARCHAR(150) UNIQUE NOT NULL, first_name VARCHAR(30) default '' NOT NULL, last_name VARCHAR(30) default '' NOT NULL, email VARCHAR(254) UNIQUE NOT NULL, is_staff BOOLEAN default false NOT NULL, is_active BOOLEAN default true NOT NULL, date_joined TIMESTAMP default now() NOT NULL ); -- add index on email CREATE INDEX IF NOT EXISTS auth_user_email_idx ON auth_user (email); CREATE TABLE IF NOT EXISTS auth_user_management ( id SERIAL PRIMARY KEY, user_id INTEGER UNIQUE NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, rate_limit INTEGER NOT NULL, created_at TIMESTAMP DEFAULT NOW() NOT NULL, updated_at TIMESTAMP DEFAULT NOW() NOT NULL ); -- add index on user_id CREATE INDEX IF NOT EXISTS auth_user_management_user_id_idx ON auth_user_management (user_id); -- control specific model ratelimit, like gpt4 -- if not find gpt4 on privilege than forbiden -- if found, then check the acess count (session messages). -- get rate_limit by user_id, chat_session_uuid CREATE TABLE IF NOT EXISTS user_chat_model_privilege( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, chat_model_id INT NOT NULL REFERENCES chat_model(id) ON DELETE CASCADE, rate_limit INTEGER NOT NULL, created_at TIMESTAMP DEFAULT NOW() NOT NULL, updated_at TIMESTAMP DEFAULT NOW() NOT NULL, created_by INTEGER NOT NULL DEFAULT 0, updated_by INTEGER NOT NULL DEFAULT 0, CONSTRAINT chat_usage_user_model_unique UNIQUE (user_id, chat_model_id) ); CREATE TABLE IF NOT EXISTS chat_workspace ( id SERIAL PRIMARY KEY, uuid VARCHAR(255) UNIQUE NOT NULL, user_id INTEGER NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, description TEXT NOT NULL DEFAULT '', color VARCHAR(7) NOT NULL DEFAULT '#6366f1', icon VARCHAR(50) NOT NULL DEFAULT 'folder', created_at TIMESTAMP DEFAULT now() NOT NULL, updated_at TIMESTAMP DEFAULT now() NOT NULL, is_default BOOLEAN DEFAULT false NOT NULL, order_position INTEGER DEFAULT 0 NOT NULL ); -- add index on user_id for workspace CREATE INDEX IF NOT EXISTS chat_workspace_user_id_idx ON chat_workspace (user_id); -- add index on uuid for workspace CREATE INDEX IF NOT EXISTS chat_workspace_uuid_idx ON chat_workspace using hash (uuid); -- Keep exactly one default workspace per user. -- If duplicates exist in old data, keep the newest default and unset the rest. WITH ranked_default_workspaces AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC, id DESC) AS row_num FROM chat_workspace WHERE is_default = true ) UPDATE chat_workspace cw SET is_default = false, updated_at = now() FROM ranked_default_workspaces rdw WHERE cw.id = rdw.id AND rdw.row_num > 1; CREATE UNIQUE INDEX IF NOT EXISTS chat_workspace_single_default_per_user_idx ON chat_workspace (user_id) WHERE is_default = true; CREATE TABLE IF NOT EXISTS chat_session ( id SERIAL PRIMARY KEY, user_id integer NOT NULL, --ALTER TABLE chat_session ADD COLUMN uuid character varying(255) NOT NULL DEFAULT ''; uuid character varying(255) UNIQUE NOT NULL, topic character varying(255) NOT NULL, created_at timestamp DEFAULT now() NOT NULL, updated_at timestamp DEFAULT now() NOT NULL, active boolean default true NOT NULL, model character varying(255) NOT NULL DEFAULT 'gpt-3.5-turbo', max_length integer DEFAULT 0 NOT NULL, temperature float DEFAULT 1.0 NOT NUll, top_p float DEFAULT 1.0 NOT NUll, max_tokens int DEFAULT 4096 NOT NULL, n integer DEFAULT 1 NOT NULL, summarize_mode boolean DEFAULT false NOT NULL, workspace_id INTEGER REFERENCES chat_workspace(id) ON DELETE SET NULL, artifact_enabled boolean DEFAULT false NOT NULL ); -- chat_session ALTER TABLE chat_session DROP COLUMN IF EXISTS code_runner_enabled; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS temperature float DEFAULT 1.0 NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS top_p float DEFAULT 1.0 NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS max_tokens int DEFAULT 4096 NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS debug boolean DEFAULT false NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS explore_mode boolean DEFAULT false NOT NULL; ALTER TABlE chat_session ADD COLUMN IF NOT EXISTS model character varying(255) NOT NULL DEFAULT 'gpt-3.5-turbo'; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS n INTEGER DEFAULT 1 NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS summarize_mode boolean DEFAULT false NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS workspace_id INTEGER REFERENCES chat_workspace(id) ON DELETE SET NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS artifact_enabled boolean DEFAULT false NOT NULL; -- add hash index on uuid CREATE INDEX IF NOT EXISTS chat_session_uuid_idx ON chat_session using hash (uuid) ; -- add index on user_id CREATE INDEX IF NOT EXISTS chat_session_user_id_idx ON chat_session (user_id); -- add index on workspace_id CREATE INDEX IF NOT EXISTS chat_session_workspace_id_idx ON chat_session (workspace_id); CREATE TABLE IF NOT EXISTS chat_message ( id SERIAL PRIMARY KEY, --ALTER TABLE chat_message ADD COLUMN uuid character varying(255) NOT NULL DEFAULT ''; uuid character varying(255) NOT NULL, chat_session_uuid character varying(255) NOT NUll, role character varying(255) NOT NULL, content character varying NOT NULL, reasoning_content character varying NOT NULL, model character varying(255) NOT NULL DEFAULT '', llm_summary character varying(1024) NOT NULL DEFAULT '', score double precision NOT NULL, user_id integer NOT NULL, created_at timestamp DEFAULT now() NOT NULL, updated_at timestamp DEFAULT now() Not NULL, created_by integer NOT NULL, updated_by integer NOT NULL, is_deleted BOOLEAN NOT NULL DEFAULT false, is_pin BOOLEAN NOT NULL DEFAULT false, token_count INTEGER DEFAULT 0 NOT NULL, raw jsonb default '{}' NOT NULL ); -- chat_messages ALTER TABLE chat_message ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN NOT NULL DEFAULT false; ALTER TABLE chat_message ADD COLUMN IF NOT EXISTS token_count INTEGER DEFAULT 0 NOT NULL; ALTER TABLE chat_message ADD COLUMN IF NOT EXISTS is_pin BOOLEAN NOT NULL DEFAULT false; ALTER TABLE chat_message ADD COLUMN IF NOT EXISTS llm_summary character varying(1024) NOT NULL DEFAULT ''; ALTER TABLE chat_message ADD COLUMN IF NOT EXISTS model character varying(255) NOT NULL DEFAULT ''; ALTER TABLE chat_message ADD COLUMN IF NOT EXISTS reasoning_content character varying NOT NULL DEFAULT ''; ALTER TABLE chat_message ADD COLUMN IF NOT EXISTS artifacts JSONB DEFAULT '[]' NOT NULL; ALTER TABLE chat_message ADD COLUMN IF NOT EXISTS suggested_questions JSONB DEFAULT '[]' NOT NULL; -- add hash index on uuid CREATE INDEX IF NOT EXISTS chat_message_uuid_idx ON chat_message using hash (uuid) ; -- add index on chat_session_uuid CREATE INDEX IF NOT EXISTS chat_message_chat_session_uuid_idx ON chat_message (chat_session_uuid); -- add index on user_id CREATE INDEX IF NOT EXISTS chat_message_user_id_idx ON chat_message (user_id); -- add brin index on created_at CREATE INDEX IF NOT EXISTS chat_message_created_at_idx ON chat_message using brin (created_at) ; CREATE TABLE IF NOT EXISTS chat_prompt ( id SERIAL PRIMARY KEY, uuid character varying(255) NOT NULL, chat_session_uuid character varying(255) NOT NULL, -- store the session_uuid role character varying(255) NOT NULL, content character varying NOT NULL, score double precision default 0 NOT NULL, user_id integer default 0 NOT NULL, created_at timestamp DEFAULT now() NOT NULL , updated_at timestamp DEFAULT now() NOT NULL, created_by integer NOT NULL, updated_by integer NOT NULL, is_deleted BOOLEAN NOT NULL DEFAULT false, token_count INTEGER DEFAULT 0 NOT NULL -- raw jsonb default '{}' NOT NULL ); ALTER TABLE chat_prompt ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN NOT NULL DEFAULT false; ALTER TABLE chat_prompt ADD COLUMN IF NOT EXISTS token_count INTEGER DEFAULT 0 NOT NULL; -- add hash index on uuid CREATE INDEX IF NOT EXISTS chat_prompt_uuid_idx ON chat_prompt using hash (uuid) ; -- add index on chat_session_uuid CREATE INDEX IF NOT EXISTS chat_prompt_chat_session_uuid_idx ON chat_prompt (chat_session_uuid); -- add index on user_id CREATE INDEX IF NOT EXISTS chat_prompt_user_id_idx ON chat_prompt (user_id); -- Keep only one active system prompt per session. -- If historical duplicates exist, soft-delete newer duplicates first so -- unique index creation succeeds in existing databases. WITH duplicate_system_prompts AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY chat_session_uuid ORDER BY id ASC) AS row_num FROM chat_prompt WHERE role = 'system' AND is_deleted = false ) UPDATE chat_prompt cp SET is_deleted = true, updated_at = now() FROM duplicate_system_prompts dsp WHERE cp.id = dsp.id AND dsp.row_num > 1; CREATE UNIQUE INDEX IF NOT EXISTS chat_prompt_unique_active_system_per_session_idx ON chat_prompt (chat_session_uuid) WHERE role = 'system' AND is_deleted = false; CREATE TABLE IF NOT EXISTS chat_logs ( id SERIAL PRIMARY KEY, -- Auto-incrementing ID as primary key session JSONB default '{}' NOT NULL, -- JSONB column to store chat session info question JSONB default '{}' NOT NULL, -- JSONB column to store the question answer JSONB default '{}' NOT NULL, -- JSONB column to store the answer created_at timestamp DEFAULT now() NOT NULL ); -- add brin index on created_at CREATE INDEX IF NOT EXISTS chat_logs_created_at_idx ON chat_logs using brin (created_at) ; -- user_id is the user who created the session -- uuid is the session uuid CREATE TABLE IF NOT EXISTS user_active_chat_session ( id SERIAL PRIMARY KEY, user_id integer UNIQUE default '0' NOT NULL , chat_session_uuid character varying(255) NOT NULL, created_at timestamp DEFAULT now() NOT NULL, updated_at timestamp DEFAULT now() NOT NULL ); -- add index on user_id CREATE INDEX IF NOT EXISTS user_active_chat_session_user_id_idx ON user_active_chat_session using hash (user_id) ; -- Extend user_active_chat_session to support per-workspace active sessions ALTER TABLE user_active_chat_session ADD COLUMN IF NOT EXISTS workspace_id INTEGER REFERENCES chat_workspace(id) ON DELETE CASCADE; -- Clean up old constraints ALTER TABLE user_active_chat_session DROP CONSTRAINT IF EXISTS user_active_chat_session_user_id_key; ALTER TABLE user_active_chat_session DROP CONSTRAINT IF EXISTS unique_user_id; -- Clean up duplicate records first -- Keep only the most recent record per user/workspace combination DELETE FROM user_active_chat_session WHERE id NOT IN ( SELECT DISTINCT ON (user_id, COALESCE(workspace_id, -1)) id FROM user_active_chat_session ORDER BY user_id, COALESCE(workspace_id, -1), updated_at DESC ); -- Create a single unique constraint using COALESCE to handle NULLs -- This treats NULL workspace_id as -1 for uniqueness purposes DROP INDEX IF EXISTS unique_user_global_active_session_idx; DROP INDEX IF EXISTS unique_user_workspace_active_session_idx; DROP INDEX IF EXISTS unique_user_workspace_active_session_coalesce_idx; CREATE UNIQUE INDEX unique_user_workspace_active_session_coalesce_idx ON user_active_chat_session (user_id, COALESCE(workspace_id, -1)); -- Add index on workspace_id for efficient queries CREATE INDEX IF NOT EXISTS user_active_chat_session_workspace_id_idx ON user_active_chat_session (workspace_id); -- for share chat feature CREATE TABLE IF NOT EXISTS chat_snapshot ( id SERIAL PRIMARY KEY, typ VARCHAR(255) NOT NULL default 'snapshot', uuid VARCHAR(255) NOT NULL default '', user_id INTEGER NOT NULL default 0, title VARCHAR(255) NOT NULL default '', summary TEXT NOT NULL default '', model VARCHAR(255) NOT NULL default '', tags JSONB DEFAULT '{}' NOT NULL, session JSONB DEFAULT '{}' NOT NULL, conversation JSONB DEFAULT '{}' NOT NULL, created_at TIMESTAMP DEFAULT now() NOT NULL, text text DEFAULT '' NOT NULL, search_vector tsvector generated always as (setweight(to_tsvector('simple', coalesce(title, '')), 'A') || ' ' || setweight(to_tsvector('simple', coalesce(text, '')), 'B') :: tsvector) stored ); ALTER TABLE chat_snapshot ADD COLUMN IF NOT EXISTS typ VARCHAR(255) NOT NULL default 'snapshot' ; ALTER TABLE chat_snapshot ADD COLUMN IF NOT EXISTS model VARCHAR(255) NOT NULL default '' ; ALTER TABLE chat_snapshot ADD COLUMN IF NOT EXISTS session JSONB DEFAULT '{}' NOT NULL; ALTER TABLE chat_snapshot ADD COLUMN IF NOT EXISTS text text DEFAULT '' NOT NULL; ALTER TABLE chat_snapshot ADD COLUMN IF NOT EXISTS search_vector tsvector generated always as ( setweight(to_tsvector('simple', coalesce(title, '')), 'A') || ' ' || setweight(to_tsvector('simple', coalesce(text, '')), 'B') :: tsvector ) stored; CREATE INDEX IF NOT EXISTS search_vector_gin_idx on chat_snapshot using GIN(search_vector); -- add index on user id CREATE INDEX IF NOT EXISTS chat_snapshot_user_id_idx ON chat_snapshot (user_id); -- add index on created_at(brin) CREATE INDEX IF NOT EXISTS chat_snapshot_created_at_idx ON chat_snapshot using brin (created_at) ; UPDATE chat_snapshot SET model = 'gpt-3.5-turbo' WHERE model = ''; CREATE TABLE IF NOT EXISTS chat_file ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, data BYTEA NOT NULL, created_at TIMESTAMP DEFAULT now() NOT NULL, user_id INTEGER NOT NULL default 1, -- foreign key chat_session_uuid chat_session_uuid VARCHAR(255) NOT NULL REFERENCES chat_session(uuid) ON DELETE CASCADE, mime_type VARCHAR(255) NOT NULL ); CREATE TABLE IF NOT EXISTS bot_answer_history ( id SERIAL PRIMARY KEY, bot_uuid VARCHAR(255) NOT NULL, user_id INTEGER NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, prompt TEXT NOT NULL, answer TEXT NOT NULL, model VARCHAR(255) NOT NULL, tokens_used INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT now() NOT NULL, updated_at TIMESTAMP DEFAULT now() NOT NULL ); -- Indexes for faster queries CREATE INDEX IF NOT EXISTS bot_answer_history_bot_uuid_idx ON bot_answer_history (bot_uuid); CREATE INDEX IF NOT EXISTS bot_answer_history_user_id_idx ON bot_answer_history (user_id); CREATE INDEX IF NOT EXISTS bot_answer_history_created_at_idx ON bot_answer_history USING BRIN (created_at); CREATE TABLE IF NOT EXISTS chat_comment ( id SERIAL PRIMARY KEY, uuid VARCHAR(255) NOT NULL, chat_session_uuid VARCHAR(255) NOT NULL, chat_message_uuid VARCHAR(255) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT now() NOT NULL, updated_at TIMESTAMP DEFAULT now() NOT NULL, created_by INTEGER NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, updated_by INTEGER NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE ); -- Add indexes for faster lookups CREATE INDEX IF NOT EXISTS chat_comment_chat_session_uuid_idx ON chat_comment (chat_session_uuid); CREATE INDEX IF NOT EXISTS chat_comment_created_by_idx ON chat_comment (created_by); ================================================ FILE: api/sqlc.yaml ================================================ version: "2" sql: - engine: "postgresql" queries: "sqlc/queries/" schema: "sqlc/schema.sql" gen: go: package: "sqlc_queries" out: "sqlc_queries" emit_json_tags: true json_tags_case_style: "camel" ================================================ FILE: api/sqlc_queries/auth_user.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: auth_user.sql package sqlc_queries import ( "context" "time" ) const createAuthUser = `-- name: CreateAuthUser :one INSERT INTO auth_user (email, "password", first_name, last_name, username, is_staff, is_superuser) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, password, last_login, is_superuser, username, first_name, last_name, email, is_staff, is_active, date_joined ` type CreateAuthUserParams struct { Email string `json:"email"` Password string `json:"password"` FirstName string `json:"firstName"` LastName string `json:"lastName"` Username string `json:"username"` IsStaff bool `json:"isStaff"` IsSuperuser bool `json:"isSuperuser"` } func (q *Queries) CreateAuthUser(ctx context.Context, arg CreateAuthUserParams) (AuthUser, error) { row := q.db.QueryRowContext(ctx, createAuthUser, arg.Email, arg.Password, arg.FirstName, arg.LastName, arg.Username, arg.IsStaff, arg.IsSuperuser, ) var i AuthUser err := row.Scan( &i.ID, &i.Password, &i.LastLogin, &i.IsSuperuser, &i.Username, &i.FirstName, &i.LastName, &i.Email, &i.IsStaff, &i.IsActive, &i.DateJoined, ) return i, err } const deleteAuthUser = `-- name: DeleteAuthUser :exec DELETE FROM auth_user WHERE email = $1 ` func (q *Queries) DeleteAuthUser(ctx context.Context, email string) error { _, err := q.db.ExecContext(ctx, deleteAuthUser, email) return err } const getAllAuthUsers = `-- name: GetAllAuthUsers :many SELECT id, password, last_login, is_superuser, username, first_name, last_name, email, is_staff, is_active, date_joined FROM auth_user ORDER BY id ` func (q *Queries) GetAllAuthUsers(ctx context.Context) ([]AuthUser, error) { rows, err := q.db.QueryContext(ctx, getAllAuthUsers) if err != nil { return nil, err } defer rows.Close() var items []AuthUser for rows.Next() { var i AuthUser if err := rows.Scan( &i.ID, &i.Password, &i.LastLogin, &i.IsSuperuser, &i.Username, &i.FirstName, &i.LastName, &i.Email, &i.IsStaff, &i.IsActive, &i.DateJoined, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getAuthUserByEmail = `-- name: GetAuthUserByEmail :one SELECT id, password, last_login, is_superuser, username, first_name, last_name, email, is_staff, is_active, date_joined FROM auth_user WHERE email = $1 ` func (q *Queries) GetAuthUserByEmail(ctx context.Context, email string) (AuthUser, error) { row := q.db.QueryRowContext(ctx, getAuthUserByEmail, email) var i AuthUser err := row.Scan( &i.ID, &i.Password, &i.LastLogin, &i.IsSuperuser, &i.Username, &i.FirstName, &i.LastName, &i.Email, &i.IsStaff, &i.IsActive, &i.DateJoined, ) return i, err } const getAuthUserByID = `-- name: GetAuthUserByID :one SELECT id, password, last_login, is_superuser, username, first_name, last_name, email, is_staff, is_active, date_joined FROM auth_user WHERE id = $1 ` func (q *Queries) GetAuthUserByID(ctx context.Context, id int32) (AuthUser, error) { row := q.db.QueryRowContext(ctx, getAuthUserByID, id) var i AuthUser err := row.Scan( &i.ID, &i.Password, &i.LastLogin, &i.IsSuperuser, &i.Username, &i.FirstName, &i.LastName, &i.Email, &i.IsStaff, &i.IsActive, &i.DateJoined, ) return i, err } const getTotalActiveUserCount = `-- name: GetTotalActiveUserCount :one SELECT COUNT(*) FROM auth_user WHERE is_active = true ` func (q *Queries) GetTotalActiveUserCount(ctx context.Context) (int64, error) { row := q.db.QueryRowContext(ctx, getTotalActiveUserCount) var count int64 err := row.Scan(&count) return count, err } const getUserAnalysisByEmail = `-- name: GetUserAnalysisByEmail :one SELECT auth_user.first_name, auth_user.last_name, auth_user.email AS user_email, COALESCE(user_stats.total_messages, 0) AS total_messages, COALESCE(user_stats.total_token_count, 0) AS total_tokens, COALESCE(user_stats.total_sessions, 0) AS total_sessions, COALESCE(user_stats.total_messages_3_days, 0) AS messages_3_days, COALESCE(user_stats.total_token_count_3_days, 0) AS tokens_3_days, COALESCE(auth_user_management.rate_limit, $2::INTEGER) AS rate_limit FROM auth_user LEFT JOIN ( SELECT stats.user_id, SUM(stats.total_messages) AS total_messages, SUM(stats.total_token_count) AS total_token_count, COUNT(DISTINCT stats.chat_session_uuid) AS total_sessions, SUM(CASE WHEN stats.created_at >= NOW() - INTERVAL '3 days' THEN stats.total_messages ELSE 0 END) AS total_messages_3_days, SUM(CASE WHEN stats.created_at >= NOW() - INTERVAL '3 days' THEN stats.total_token_count ELSE 0 END) AS total_token_count_3_days FROM ( SELECT user_id, chat_session_uuid, COUNT(*) AS total_messages, SUM(token_count) as total_token_count, MAX(created_at) AS created_at FROM chat_message WHERE is_deleted = false GROUP BY user_id, chat_session_uuid ) AS stats GROUP BY stats.user_id ) AS user_stats ON auth_user.id = user_stats.user_id LEFT JOIN auth_user_management ON auth_user.id = auth_user_management.user_id WHERE auth_user.email = $1 ` type GetUserAnalysisByEmailParams struct { Email string `json:"email"` DefaultRateLimit int32 `json:"defaultRateLimit"` } type GetUserAnalysisByEmailRow struct { FirstName string `json:"firstName"` LastName string `json:"lastName"` UserEmail string `json:"userEmail"` TotalMessages int64 `json:"totalMessages"` TotalTokens int64 `json:"totalTokens"` TotalSessions int64 `json:"totalSessions"` Messages3Days int64 `json:"messages3Days"` Tokens3Days int64 `json:"tokens3Days"` RateLimit int32 `json:"rateLimit"` } func (q *Queries) GetUserAnalysisByEmail(ctx context.Context, arg GetUserAnalysisByEmailParams) (GetUserAnalysisByEmailRow, error) { row := q.db.QueryRowContext(ctx, getUserAnalysisByEmail, arg.Email, arg.DefaultRateLimit) var i GetUserAnalysisByEmailRow err := row.Scan( &i.FirstName, &i.LastName, &i.UserEmail, &i.TotalMessages, &i.TotalTokens, &i.TotalSessions, &i.Messages3Days, &i.Tokens3Days, &i.RateLimit, ) return i, err } const getUserByEmail = `-- name: GetUserByEmail :one SELECT id, password, last_login, is_superuser, username, first_name, last_name, email, is_staff, is_active, date_joined FROM auth_user WHERE email = $1 ` func (q *Queries) GetUserByEmail(ctx context.Context, email string) (AuthUser, error) { row := q.db.QueryRowContext(ctx, getUserByEmail, email) var i AuthUser err := row.Scan( &i.ID, &i.Password, &i.LastLogin, &i.IsSuperuser, &i.Username, &i.FirstName, &i.LastName, &i.Email, &i.IsStaff, &i.IsActive, &i.DateJoined, ) return i, err } const getUserModelUsageByEmail = `-- name: GetUserModelUsageByEmail :many SELECT COALESCE(cm.model, 'unknown') AS model, COUNT(*) AS message_count, COALESCE(SUM(cm.token_count), 0) AS token_count, MAX(cm.created_at)::timestamp AS last_used FROM chat_message cm INNER JOIN auth_user au ON cm.user_id = au.id WHERE au.email = $1 AND cm.is_deleted = false AND cm.role = 'assistant' AND cm.model IS NOT NULL AND cm.model != '' GROUP BY cm.model ORDER BY message_count DESC ` type GetUserModelUsageByEmailRow struct { Model string `json:"model"` MessageCount int64 `json:"messageCount"` TokenCount interface{} `json:"tokenCount"` LastUsed time.Time `json:"lastUsed"` } func (q *Queries) GetUserModelUsageByEmail(ctx context.Context, email string) ([]GetUserModelUsageByEmailRow, error) { rows, err := q.db.QueryContext(ctx, getUserModelUsageByEmail, email) if err != nil { return nil, err } defer rows.Close() var items []GetUserModelUsageByEmailRow for rows.Next() { var i GetUserModelUsageByEmailRow if err := rows.Scan( &i.Model, &i.MessageCount, &i.TokenCount, &i.LastUsed, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getUserRecentActivityByEmail = `-- name: GetUserRecentActivityByEmail :many SELECT DATE(cm.created_at) AS activity_date, COUNT(*) AS messages, COALESCE(SUM(cm.token_count), 0) AS tokens, COUNT(DISTINCT cm.chat_session_uuid) AS sessions FROM chat_message cm INNER JOIN auth_user au ON cm.user_id = au.id WHERE au.email = $1 AND cm.is_deleted = false AND cm.created_at >= NOW() - INTERVAL '30 days' GROUP BY DATE(cm.created_at) ORDER BY activity_date DESC LIMIT 30 ` type GetUserRecentActivityByEmailRow struct { ActivityDate time.Time `json:"activityDate"` Messages int64 `json:"messages"` Tokens interface{} `json:"tokens"` Sessions int64 `json:"sessions"` } func (q *Queries) GetUserRecentActivityByEmail(ctx context.Context, email string) ([]GetUserRecentActivityByEmailRow, error) { rows, err := q.db.QueryContext(ctx, getUserRecentActivityByEmail, email) if err != nil { return nil, err } defer rows.Close() var items []GetUserRecentActivityByEmailRow for rows.Next() { var i GetUserRecentActivityByEmailRow if err := rows.Scan( &i.ActivityDate, &i.Messages, &i.Tokens, &i.Sessions, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getUserSessionHistoryByEmail = `-- name: GetUserSessionHistoryByEmail :many SELECT cs.uuid AS session_id, cs.model, COALESCE(COUNT(cm.id), 0) AS message_count, COALESCE(SUM(cm.token_count), 0) AS token_count, COALESCE(MIN(cm.created_at), cs.created_at)::timestamp AS created_at, COALESCE(MAX(cm.created_at), cs.updated_at)::timestamp AS updated_at FROM chat_session cs INNER JOIN auth_user au ON cs.user_id = au.id LEFT JOIN chat_message cm ON cs.uuid = cm.chat_session_uuid AND cm.is_deleted = false WHERE au.email = $1 AND cs.active = true GROUP BY cs.uuid, cs.model, cs.created_at, cs.updated_at ORDER BY cs.updated_at DESC LIMIT $2 OFFSET $3 ` type GetUserSessionHistoryByEmailParams struct { Email string `json:"email"` Limit int32 `json:"limit"` Offset int32 `json:"offset"` } type GetUserSessionHistoryByEmailRow struct { SessionID string `json:"sessionId"` Model string `json:"model"` MessageCount interface{} `json:"messageCount"` TokenCount interface{} `json:"tokenCount"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } func (q *Queries) GetUserSessionHistoryByEmail(ctx context.Context, arg GetUserSessionHistoryByEmailParams) ([]GetUserSessionHistoryByEmailRow, error) { rows, err := q.db.QueryContext(ctx, getUserSessionHistoryByEmail, arg.Email, arg.Limit, arg.Offset) if err != nil { return nil, err } defer rows.Close() var items []GetUserSessionHistoryByEmailRow for rows.Next() { var i GetUserSessionHistoryByEmailRow if err := rows.Scan( &i.SessionID, &i.Model, &i.MessageCount, &i.TokenCount, &i.CreatedAt, &i.UpdatedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getUserSessionHistoryCountByEmail = `-- name: GetUserSessionHistoryCountByEmail :one SELECT COUNT(DISTINCT cs.uuid) AS total_sessions FROM chat_session cs INNER JOIN auth_user au ON cs.user_id = au.id WHERE au.email = $1 AND cs.active = true ` func (q *Queries) GetUserSessionHistoryCountByEmail(ctx context.Context, email string) (int64, error) { row := q.db.QueryRowContext(ctx, getUserSessionHistoryCountByEmail, email) var total_sessions int64 err := row.Scan(&total_sessions) return total_sessions, err } const getUserStats = `-- name: GetUserStats :many SELECT auth_user.first_name, auth_user.last_name, auth_user.email AS user_email, COALESCE(user_stats.total_messages, 0) AS total_chat_messages, COALESCE(user_stats.total_token_count, 0) AS total_token_count, COALESCE(user_stats.total_messages_3_days, 0) AS total_chat_messages_3_days, COALESCE(user_stats.total_token_count_3_days, 0) AS total_token_count_3_days, COALESCE(auth_user_management.rate_limit, $3::INTEGER) AS rate_limit FROM auth_user LEFT JOIN ( SELECT chat_message_stats.user_id, SUM(total_messages) AS total_messages, SUM(total_token_count) AS total_token_count, SUM(CASE WHEN created_at >= NOW() - INTERVAL '3 days' THEN total_messages ELSE 0 END) AS total_messages_3_days, SUM(CASE WHEN created_at >= NOW() - INTERVAL '3 days' THEN total_token_count ELSE 0 END) AS total_token_count_3_days FROM ( SELECT user_id, COUNT(*) AS total_messages, SUM(token_count) as total_token_count, MAX(created_at) AS created_at FROM chat_message GROUP BY user_id, chat_session_uuid ) AS chat_message_stats GROUP BY chat_message_stats.user_id ) AS user_stats ON auth_user.id = user_stats.user_id LEFT JOIN auth_user_management ON auth_user.id = auth_user_management.user_id ORDER BY total_chat_messages DESC, auth_user.id DESC OFFSET $2 LIMIT $1 ` type GetUserStatsParams struct { Limit int32 `json:"limit"` Offset int32 `json:"offset"` DefaultRateLimit int32 `json:"defaultRateLimit"` } type GetUserStatsRow struct { FirstName string `json:"firstName"` LastName string `json:"lastName"` UserEmail string `json:"userEmail"` TotalChatMessages int64 `json:"totalChatMessages"` TotalTokenCount int64 `json:"totalTokenCount"` TotalChatMessages3Days int64 `json:"totalChatMessages3Days"` TotalTokenCount3Days int64 `json:"totalTokenCount3Days"` RateLimit int32 `json:"rateLimit"` } func (q *Queries) GetUserStats(ctx context.Context, arg GetUserStatsParams) ([]GetUserStatsRow, error) { rows, err := q.db.QueryContext(ctx, getUserStats, arg.Limit, arg.Offset, arg.DefaultRateLimit) if err != nil { return nil, err } defer rows.Close() var items []GetUserStatsRow for rows.Next() { var i GetUserStatsRow if err := rows.Scan( &i.FirstName, &i.LastName, &i.UserEmail, &i.TotalChatMessages, &i.TotalTokenCount, &i.TotalChatMessages3Days, &i.TotalTokenCount3Days, &i.RateLimit, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listAuthUsers = `-- name: ListAuthUsers :many SELECT id, password, last_login, is_superuser, username, first_name, last_name, email, is_staff, is_active, date_joined FROM auth_user ORDER BY id LIMIT $1 OFFSET $2 ` type ListAuthUsersParams struct { Limit int32 `json:"limit"` Offset int32 `json:"offset"` } func (q *Queries) ListAuthUsers(ctx context.Context, arg ListAuthUsersParams) ([]AuthUser, error) { rows, err := q.db.QueryContext(ctx, listAuthUsers, arg.Limit, arg.Offset) if err != nil { return nil, err } defer rows.Close() var items []AuthUser for rows.Next() { var i AuthUser if err := rows.Scan( &i.ID, &i.Password, &i.LastLogin, &i.IsSuperuser, &i.Username, &i.FirstName, &i.LastName, &i.Email, &i.IsStaff, &i.IsActive, &i.DateJoined, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const updateAuthUser = `-- name: UpdateAuthUser :one UPDATE auth_user SET first_name = $2, last_name= $3, last_login = now() WHERE id = $1 RETURNING first_name, last_name, email ` type UpdateAuthUserParams struct { ID int32 `json:"id"` FirstName string `json:"firstName"` LastName string `json:"lastName"` } type UpdateAuthUserRow struct { FirstName string `json:"firstName"` LastName string `json:"lastName"` Email string `json:"email"` } func (q *Queries) UpdateAuthUser(ctx context.Context, arg UpdateAuthUserParams) (UpdateAuthUserRow, error) { row := q.db.QueryRowContext(ctx, updateAuthUser, arg.ID, arg.FirstName, arg.LastName) var i UpdateAuthUserRow err := row.Scan(&i.FirstName, &i.LastName, &i.Email) return i, err } const updateAuthUserByEmail = `-- name: UpdateAuthUserByEmail :one UPDATE auth_user SET first_name = $2, last_name= $3, last_login = now() WHERE email = $1 RETURNING first_name, last_name, email ` type UpdateAuthUserByEmailParams struct { Email string `json:"email"` FirstName string `json:"firstName"` LastName string `json:"lastName"` } type UpdateAuthUserByEmailRow struct { FirstName string `json:"firstName"` LastName string `json:"lastName"` Email string `json:"email"` } func (q *Queries) UpdateAuthUserByEmail(ctx context.Context, arg UpdateAuthUserByEmailParams) (UpdateAuthUserByEmailRow, error) { row := q.db.QueryRowContext(ctx, updateAuthUserByEmail, arg.Email, arg.FirstName, arg.LastName) var i UpdateAuthUserByEmailRow err := row.Scan(&i.FirstName, &i.LastName, &i.Email) return i, err } const updateAuthUserRateLimitByEmail = `-- name: UpdateAuthUserRateLimitByEmail :one INSERT INTO auth_user_management (user_id, rate_limit, created_at, updated_at) VALUES ((SELECT id FROM auth_user WHERE email = $1), $2, NOW(), NOW()) ON CONFLICT (user_id) DO UPDATE SET rate_limit = $2, updated_at = NOW() RETURNING rate_limit ` type UpdateAuthUserRateLimitByEmailParams struct { Email string `json:"email"` RateLimit int32 `json:"rateLimit"` } func (q *Queries) UpdateAuthUserRateLimitByEmail(ctx context.Context, arg UpdateAuthUserRateLimitByEmailParams) (int32, error) { row := q.db.QueryRowContext(ctx, updateAuthUserRateLimitByEmail, arg.Email, arg.RateLimit) var rate_limit int32 err := row.Scan(&rate_limit) return rate_limit, err } const updateUserPassword = `-- name: UpdateUserPassword :exec UPDATE auth_user SET "password" = $2 WHERE email = $1 ` type UpdateUserPasswordParams struct { Email string `json:"email"` Password string `json:"password"` } func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error { _, err := q.db.ExecContext(ctx, updateUserPassword, arg.Email, arg.Password) return err } ================================================ FILE: api/sqlc_queries/auth_user_management.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: auth_user_management.sql package sqlc_queries import ( "context" ) const getRateLimit = `-- name: GetRateLimit :one SELECT rate_limit AS rate_limit FROM auth_user_management WHERE user_id = $1 ` // GetRateLimit retrieves the rate limit for a user from the auth_user_management table. // If no rate limit is set for the user, it returns the default rate limit of 100. func (q *Queries) GetRateLimit(ctx context.Context, userID int32) (int32, error) { row := q.db.QueryRowContext(ctx, getRateLimit, userID) var rate_limit int32 err := row.Scan(&rate_limit) return rate_limit, err } ================================================ FILE: api/sqlc_queries/bot_answer_history.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: bot_answer_history.sql package sqlc_queries import ( "context" "time" ) const createBotAnswerHistory = `-- name: CreateBotAnswerHistory :one INSERT INTO bot_answer_history ( bot_uuid, user_id, prompt, answer, model, tokens_used ) VALUES ( $1, $2, $3, $4, $5, $6 ) RETURNING id, bot_uuid, user_id, prompt, answer, model, tokens_used, created_at, updated_at ` type CreateBotAnswerHistoryParams struct { BotUuid string `json:"botUuid"` UserID int32 `json:"userId"` Prompt string `json:"prompt"` Answer string `json:"answer"` Model string `json:"model"` TokensUsed int32 `json:"tokensUsed"` } // Bot Answer History Queries -- func (q *Queries) CreateBotAnswerHistory(ctx context.Context, arg CreateBotAnswerHistoryParams) (BotAnswerHistory, error) { row := q.db.QueryRowContext(ctx, createBotAnswerHistory, arg.BotUuid, arg.UserID, arg.Prompt, arg.Answer, arg.Model, arg.TokensUsed, ) var i BotAnswerHistory err := row.Scan( &i.ID, &i.BotUuid, &i.UserID, &i.Prompt, &i.Answer, &i.Model, &i.TokensUsed, &i.CreatedAt, &i.UpdatedAt, ) return i, err } const deleteBotAnswerHistory = `-- name: DeleteBotAnswerHistory :exec DELETE FROM bot_answer_history WHERE id = $1 ` func (q *Queries) DeleteBotAnswerHistory(ctx context.Context, id int32) error { _, err := q.db.ExecContext(ctx, deleteBotAnswerHistory, id) return err } const getBotAnswerHistoryByBotUUID = `-- name: GetBotAnswerHistoryByBotUUID :many SELECT bah.id, bah.bot_uuid, bah.user_id, bah.prompt, bah.answer, bah.model, bah.tokens_used, bah.created_at, bah.updated_at, au.username AS user_username, au.email AS user_email FROM bot_answer_history bah JOIN auth_user au ON bah.user_id = au.id WHERE bah.bot_uuid = $1 ORDER BY bah.created_at DESC LIMIT $2 OFFSET $3 ` type GetBotAnswerHistoryByBotUUIDParams struct { BotUuid string `json:"botUuid"` Limit int32 `json:"limit"` Offset int32 `json:"offset"` } type GetBotAnswerHistoryByBotUUIDRow struct { ID int32 `json:"id"` BotUuid string `json:"botUuid"` UserID int32 `json:"userId"` Prompt string `json:"prompt"` Answer string `json:"answer"` Model string `json:"model"` TokensUsed int32 `json:"tokensUsed"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` UserUsername string `json:"userUsername"` UserEmail string `json:"userEmail"` } func (q *Queries) GetBotAnswerHistoryByBotUUID(ctx context.Context, arg GetBotAnswerHistoryByBotUUIDParams) ([]GetBotAnswerHistoryByBotUUIDRow, error) { rows, err := q.db.QueryContext(ctx, getBotAnswerHistoryByBotUUID, arg.BotUuid, arg.Limit, arg.Offset) if err != nil { return nil, err } defer rows.Close() var items []GetBotAnswerHistoryByBotUUIDRow for rows.Next() { var i GetBotAnswerHistoryByBotUUIDRow if err := rows.Scan( &i.ID, &i.BotUuid, &i.UserID, &i.Prompt, &i.Answer, &i.Model, &i.TokensUsed, &i.CreatedAt, &i.UpdatedAt, &i.UserUsername, &i.UserEmail, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getBotAnswerHistoryByID = `-- name: GetBotAnswerHistoryByID :one SELECT bah.id, bah.bot_uuid, bah.user_id, bah.prompt, bah.answer, bah.model, bah.tokens_used, bah.created_at, bah.updated_at, au.username AS user_username, au.email AS user_email FROM bot_answer_history bah JOIN auth_user au ON bah.user_id = au.id WHERE bah.id = $1 ` type GetBotAnswerHistoryByIDRow struct { ID int32 `json:"id"` BotUuid string `json:"botUuid"` UserID int32 `json:"userId"` Prompt string `json:"prompt"` Answer string `json:"answer"` Model string `json:"model"` TokensUsed int32 `json:"tokensUsed"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` UserUsername string `json:"userUsername"` UserEmail string `json:"userEmail"` } func (q *Queries) GetBotAnswerHistoryByID(ctx context.Context, id int32) (GetBotAnswerHistoryByIDRow, error) { row := q.db.QueryRowContext(ctx, getBotAnswerHistoryByID, id) var i GetBotAnswerHistoryByIDRow err := row.Scan( &i.ID, &i.BotUuid, &i.UserID, &i.Prompt, &i.Answer, &i.Model, &i.TokensUsed, &i.CreatedAt, &i.UpdatedAt, &i.UserUsername, &i.UserEmail, ) return i, err } const getBotAnswerHistoryByUserID = `-- name: GetBotAnswerHistoryByUserID :many SELECT bah.id, bah.bot_uuid, bah.user_id, bah.prompt, bah.answer, bah.model, bah.tokens_used, bah.created_at, bah.updated_at, au.username AS user_username, au.email AS user_email FROM bot_answer_history bah JOIN auth_user au ON bah.user_id = au.id WHERE bah.user_id = $1 ORDER BY bah.created_at DESC LIMIT $2 OFFSET $3 ` type GetBotAnswerHistoryByUserIDParams struct { UserID int32 `json:"userId"` Limit int32 `json:"limit"` Offset int32 `json:"offset"` } type GetBotAnswerHistoryByUserIDRow struct { ID int32 `json:"id"` BotUuid string `json:"botUuid"` UserID int32 `json:"userId"` Prompt string `json:"prompt"` Answer string `json:"answer"` Model string `json:"model"` TokensUsed int32 `json:"tokensUsed"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` UserUsername string `json:"userUsername"` UserEmail string `json:"userEmail"` } func (q *Queries) GetBotAnswerHistoryByUserID(ctx context.Context, arg GetBotAnswerHistoryByUserIDParams) ([]GetBotAnswerHistoryByUserIDRow, error) { rows, err := q.db.QueryContext(ctx, getBotAnswerHistoryByUserID, arg.UserID, arg.Limit, arg.Offset) if err != nil { return nil, err } defer rows.Close() var items []GetBotAnswerHistoryByUserIDRow for rows.Next() { var i GetBotAnswerHistoryByUserIDRow if err := rows.Scan( &i.ID, &i.BotUuid, &i.UserID, &i.Prompt, &i.Answer, &i.Model, &i.TokensUsed, &i.CreatedAt, &i.UpdatedAt, &i.UserUsername, &i.UserEmail, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getBotAnswerHistoryCountByBotUUID = `-- name: GetBotAnswerHistoryCountByBotUUID :one SELECT COUNT(*) FROM bot_answer_history WHERE bot_uuid = $1 ` func (q *Queries) GetBotAnswerHistoryCountByBotUUID(ctx context.Context, botUuid string) (int64, error) { row := q.db.QueryRowContext(ctx, getBotAnswerHistoryCountByBotUUID, botUuid) var count int64 err := row.Scan(&count) return count, err } const getBotAnswerHistoryCountByUserID = `-- name: GetBotAnswerHistoryCountByUserID :one SELECT COUNT(*) FROM bot_answer_history WHERE user_id = $1 ` func (q *Queries) GetBotAnswerHistoryCountByUserID(ctx context.Context, userID int32) (int64, error) { row := q.db.QueryRowContext(ctx, getBotAnswerHistoryCountByUserID, userID) var count int64 err := row.Scan(&count) return count, err } const getLatestBotAnswerHistoryByBotUUID = `-- name: GetLatestBotAnswerHistoryByBotUUID :many SELECT bah.id, bah.bot_uuid, bah.user_id, bah.prompt, bah.answer, bah.model, bah.tokens_used, bah.created_at, bah.updated_at, au.username AS user_username, au.email AS user_email FROM bot_answer_history bah JOIN auth_user au ON bah.user_id = au.id WHERE bah.bot_uuid = $1 ORDER BY bah.created_at DESC LIMIT $2 ` type GetLatestBotAnswerHistoryByBotUUIDParams struct { BotUuid string `json:"botUuid"` Limit int32 `json:"limit"` } type GetLatestBotAnswerHistoryByBotUUIDRow struct { ID int32 `json:"id"` BotUuid string `json:"botUuid"` UserID int32 `json:"userId"` Prompt string `json:"prompt"` Answer string `json:"answer"` Model string `json:"model"` TokensUsed int32 `json:"tokensUsed"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` UserUsername string `json:"userUsername"` UserEmail string `json:"userEmail"` } func (q *Queries) GetLatestBotAnswerHistoryByBotUUID(ctx context.Context, arg GetLatestBotAnswerHistoryByBotUUIDParams) ([]GetLatestBotAnswerHistoryByBotUUIDRow, error) { rows, err := q.db.QueryContext(ctx, getLatestBotAnswerHistoryByBotUUID, arg.BotUuid, arg.Limit) if err != nil { return nil, err } defer rows.Close() var items []GetLatestBotAnswerHistoryByBotUUIDRow for rows.Next() { var i GetLatestBotAnswerHistoryByBotUUIDRow if err := rows.Scan( &i.ID, &i.BotUuid, &i.UserID, &i.Prompt, &i.Answer, &i.Model, &i.TokensUsed, &i.CreatedAt, &i.UpdatedAt, &i.UserUsername, &i.UserEmail, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const updateBotAnswerHistory = `-- name: UpdateBotAnswerHistory :one UPDATE bot_answer_history SET answer = $2, tokens_used = $3, updated_at = NOW() WHERE id = $1 RETURNING id, bot_uuid, user_id, prompt, answer, model, tokens_used, created_at, updated_at ` type UpdateBotAnswerHistoryParams struct { ID int32 `json:"id"` Answer string `json:"answer"` TokensUsed int32 `json:"tokensUsed"` } func (q *Queries) UpdateBotAnswerHistory(ctx context.Context, arg UpdateBotAnswerHistoryParams) (BotAnswerHistory, error) { row := q.db.QueryRowContext(ctx, updateBotAnswerHistory, arg.ID, arg.Answer, arg.TokensUsed) var i BotAnswerHistory err := row.Scan( &i.ID, &i.BotUuid, &i.UserID, &i.Prompt, &i.Answer, &i.Model, &i.TokensUsed, &i.CreatedAt, &i.UpdatedAt, ) return i, err } ================================================ FILE: api/sqlc_queries/chat_comment.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: chat_comment.sql package sqlc_queries import ( "context" "time" ) const createChatComment = `-- name: CreateChatComment :one INSERT INTO chat_comment ( uuid, chat_session_uuid, chat_message_uuid, content, created_by, updated_by ) VALUES ( $1, $2, $3, $4, $5, $5 ) RETURNING id, uuid, chat_session_uuid, chat_message_uuid, content, created_at, updated_at, created_by, updated_by ` type CreateChatCommentParams struct { Uuid string `json:"uuid"` ChatSessionUuid string `json:"chatSessionUuid"` ChatMessageUuid string `json:"chatMessageUuid"` Content string `json:"content"` CreatedBy int32 `json:"createdBy"` } func (q *Queries) CreateChatComment(ctx context.Context, arg CreateChatCommentParams) (ChatComment, error) { row := q.db.QueryRowContext(ctx, createChatComment, arg.Uuid, arg.ChatSessionUuid, arg.ChatMessageUuid, arg.Content, arg.CreatedBy, ) var i ChatComment err := row.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.ChatMessageUuid, &i.Content, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, ) return i, err } const getCommentsByMessageUUID = `-- name: GetCommentsByMessageUUID :many SELECT cc.uuid, cc.content, cc.created_at, au.username AS author_username, au.email AS author_email FROM chat_comment cc JOIN auth_user au ON cc.created_by = au.id WHERE cc.chat_message_uuid = $1 ORDER BY cc.created_at DESC ` type GetCommentsByMessageUUIDRow struct { Uuid string `json:"uuid"` Content string `json:"content"` CreatedAt time.Time `json:"createdAt"` AuthorUsername string `json:"authorUsername"` AuthorEmail string `json:"authorEmail"` } func (q *Queries) GetCommentsByMessageUUID(ctx context.Context, chatMessageUuid string) ([]GetCommentsByMessageUUIDRow, error) { rows, err := q.db.QueryContext(ctx, getCommentsByMessageUUID, chatMessageUuid) if err != nil { return nil, err } defer rows.Close() var items []GetCommentsByMessageUUIDRow for rows.Next() { var i GetCommentsByMessageUUIDRow if err := rows.Scan( &i.Uuid, &i.Content, &i.CreatedAt, &i.AuthorUsername, &i.AuthorEmail, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getCommentsBySessionUUID = `-- name: GetCommentsBySessionUUID :many SELECT cc.uuid, cc.chat_message_uuid, cc.content, cc.created_at, au.username AS author_username, au.email AS author_email FROM chat_comment cc JOIN auth_user au ON cc.created_by = au.id WHERE cc.chat_session_uuid = $1 ORDER BY cc.created_at DESC ` type GetCommentsBySessionUUIDRow struct { Uuid string `json:"uuid"` ChatMessageUuid string `json:"chatMessageUuid"` Content string `json:"content"` CreatedAt time.Time `json:"createdAt"` AuthorUsername string `json:"authorUsername"` AuthorEmail string `json:"authorEmail"` } func (q *Queries) GetCommentsBySessionUUID(ctx context.Context, chatSessionUuid string) ([]GetCommentsBySessionUUIDRow, error) { rows, err := q.db.QueryContext(ctx, getCommentsBySessionUUID, chatSessionUuid) if err != nil { return nil, err } defer rows.Close() var items []GetCommentsBySessionUUIDRow for rows.Next() { var i GetCommentsBySessionUUIDRow if err := rows.Scan( &i.Uuid, &i.ChatMessageUuid, &i.Content, &i.CreatedAt, &i.AuthorUsername, &i.AuthorEmail, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } ================================================ FILE: api/sqlc_queries/chat_file.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: chat_file.sql package sqlc_queries import ( "context" "time" ) const createChatFile = `-- name: CreateChatFile :one INSERT INTO chat_file (name, data, user_id, chat_session_uuid, mime_type) VALUES ($1, $2, $3, $4, $5) RETURNING id, name, data, created_at, user_id, chat_session_uuid, mime_type ` type CreateChatFileParams struct { Name string `json:"name"` Data []byte `json:"data"` UserID int32 `json:"userId"` ChatSessionUuid string `json:"chatSessionUuid"` MimeType string `json:"mimeType"` } func (q *Queries) CreateChatFile(ctx context.Context, arg CreateChatFileParams) (ChatFile, error) { row := q.db.QueryRowContext(ctx, createChatFile, arg.Name, arg.Data, arg.UserID, arg.ChatSessionUuid, arg.MimeType, ) var i ChatFile err := row.Scan( &i.ID, &i.Name, &i.Data, &i.CreatedAt, &i.UserID, &i.ChatSessionUuid, &i.MimeType, ) return i, err } const deleteChatFile = `-- name: DeleteChatFile :one DELETE FROM chat_file WHERE id = $1 RETURNING id, name, data, created_at, user_id, chat_session_uuid, mime_type ` func (q *Queries) DeleteChatFile(ctx context.Context, id int32) (ChatFile, error) { row := q.db.QueryRowContext(ctx, deleteChatFile, id) var i ChatFile err := row.Scan( &i.ID, &i.Name, &i.Data, &i.CreatedAt, &i.UserID, &i.ChatSessionUuid, &i.MimeType, ) return i, err } const getChatFileByID = `-- name: GetChatFileByID :one SELECT id, name, data, created_at, user_id, chat_session_uuid FROM chat_file WHERE id = $1 ` type GetChatFileByIDRow struct { ID int32 `json:"id"` Name string `json:"name"` Data []byte `json:"data"` CreatedAt time.Time `json:"createdAt"` UserID int32 `json:"userId"` ChatSessionUuid string `json:"chatSessionUuid"` } func (q *Queries) GetChatFileByID(ctx context.Context, id int32) (GetChatFileByIDRow, error) { row := q.db.QueryRowContext(ctx, getChatFileByID, id) var i GetChatFileByIDRow err := row.Scan( &i.ID, &i.Name, &i.Data, &i.CreatedAt, &i.UserID, &i.ChatSessionUuid, ) return i, err } const listChatFilesBySessionUUID = `-- name: ListChatFilesBySessionUUID :many SELECT id, name FROM chat_file WHERE user_id = $1 and chat_session_uuid = $2 ORDER BY created_at ` type ListChatFilesBySessionUUIDParams struct { UserID int32 `json:"userId"` ChatSessionUuid string `json:"chatSessionUuid"` } type ListChatFilesBySessionUUIDRow struct { ID int32 `json:"id"` Name string `json:"name"` } func (q *Queries) ListChatFilesBySessionUUID(ctx context.Context, arg ListChatFilesBySessionUUIDParams) ([]ListChatFilesBySessionUUIDRow, error) { rows, err := q.db.QueryContext(ctx, listChatFilesBySessionUUID, arg.UserID, arg.ChatSessionUuid) if err != nil { return nil, err } defer rows.Close() var items []ListChatFilesBySessionUUIDRow for rows.Next() { var i ListChatFilesBySessionUUIDRow if err := rows.Scan(&i.ID, &i.Name); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listChatFilesWithContentBySessionUUID = `-- name: ListChatFilesWithContentBySessionUUID :many SELECT id, name, data, created_at, user_id, chat_session_uuid, mime_type FROM chat_file WHERE chat_session_uuid = $1 ORDER BY created_at ` func (q *Queries) ListChatFilesWithContentBySessionUUID(ctx context.Context, chatSessionUuid string) ([]ChatFile, error) { rows, err := q.db.QueryContext(ctx, listChatFilesWithContentBySessionUUID, chatSessionUuid) if err != nil { return nil, err } defer rows.Close() var items []ChatFile for rows.Next() { var i ChatFile if err := rows.Scan( &i.ID, &i.Name, &i.Data, &i.CreatedAt, &i.UserID, &i.ChatSessionUuid, &i.MimeType, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } ================================================ FILE: api/sqlc_queries/chat_log.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: chat_log.sql package sqlc_queries import ( "context" "encoding/json" ) const chatLogByID = `-- name: ChatLogByID :one SELECT id, session, question, answer, created_at FROM chat_logs WHERE id = $1 ` func (q *Queries) ChatLogByID(ctx context.Context, id int32) (ChatLog, error) { row := q.db.QueryRowContext(ctx, chatLogByID, id) var i ChatLog err := row.Scan( &i.ID, &i.Session, &i.Question, &i.Answer, &i.CreatedAt, ) return i, err } const createChatLog = `-- name: CreateChatLog :one INSERT INTO chat_logs (session, question, answer) VALUES ($1, $2, $3) RETURNING id, session, question, answer, created_at ` type CreateChatLogParams struct { Session json.RawMessage `json:"session"` Question json.RawMessage `json:"question"` Answer json.RawMessage `json:"answer"` } func (q *Queries) CreateChatLog(ctx context.Context, arg CreateChatLogParams) (ChatLog, error) { row := q.db.QueryRowContext(ctx, createChatLog, arg.Session, arg.Question, arg.Answer) var i ChatLog err := row.Scan( &i.ID, &i.Session, &i.Question, &i.Answer, &i.CreatedAt, ) return i, err } const deleteChatLog = `-- name: DeleteChatLog :exec DELETE FROM chat_logs WHERE id = $1 ` func (q *Queries) DeleteChatLog(ctx context.Context, id int32) error { _, err := q.db.ExecContext(ctx, deleteChatLog, id) return err } const listChatLogs = `-- name: ListChatLogs :many SELECT id, session, question, answer, created_at FROM chat_logs ORDER BY id ` func (q *Queries) ListChatLogs(ctx context.Context) ([]ChatLog, error) { rows, err := q.db.QueryContext(ctx, listChatLogs) if err != nil { return nil, err } defer rows.Close() var items []ChatLog for rows.Next() { var i ChatLog if err := rows.Scan( &i.ID, &i.Session, &i.Question, &i.Answer, &i.CreatedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const updateChatLog = `-- name: UpdateChatLog :one UPDATE chat_logs SET session = $2, question = $3, answer = $4 WHERE id = $1 RETURNING id, session, question, answer, created_at ` type UpdateChatLogParams struct { ID int32 `json:"id"` Session json.RawMessage `json:"session"` Question json.RawMessage `json:"question"` Answer json.RawMessage `json:"answer"` } func (q *Queries) UpdateChatLog(ctx context.Context, arg UpdateChatLogParams) (ChatLog, error) { row := q.db.QueryRowContext(ctx, updateChatLog, arg.ID, arg.Session, arg.Question, arg.Answer, ) var i ChatLog err := row.Scan( &i.ID, &i.Session, &i.Question, &i.Answer, &i.CreatedAt, ) return i, err } ================================================ FILE: api/sqlc_queries/chat_message.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: chat_message.sql package sqlc_queries import ( "context" "encoding/json" "time" ) const createChatMessage = `-- name: CreateChatMessage :one INSERT INTO chat_message (chat_session_uuid, uuid, role, content, reasoning_content, model, token_count, score, user_id, created_by, updated_by, llm_summary, raw, artifacts, suggested_questions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) 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 CreateChatMessageParams struct { ChatSessionUuid string `json:"chatSessionUuid"` Uuid string `json:"uuid"` Role string `json:"role"` Content string `json:"content"` ReasoningContent string `json:"reasoningContent"` Model string `json:"model"` TokenCount int32 `json:"tokenCount"` Score float64 `json:"score"` UserID int32 `json:"userId"` CreatedBy int32 `json:"createdBy"` UpdatedBy int32 `json:"updatedBy"` LlmSummary string `json:"llmSummary"` Raw json.RawMessage `json:"raw"` Artifacts json.RawMessage `json:"artifacts"` SuggestedQuestions json.RawMessage `json:"suggestedQuestions"` } func (q *Queries) CreateChatMessage(ctx context.Context, arg CreateChatMessageParams) (ChatMessage, error) { row := q.db.QueryRowContext(ctx, createChatMessage, arg.ChatSessionUuid, arg.Uuid, arg.Role, arg.Content, arg.ReasoningContent, arg.Model, arg.TokenCount, arg.Score, arg.UserID, arg.CreatedBy, arg.UpdatedBy, arg.LlmSummary, arg.Raw, arg.Artifacts, 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 } const deleteChatMessage = `-- name: DeleteChatMessage :exec UPDATE chat_message set is_deleted = true, updated_at = now() WHERE id = $1 ` func (q *Queries) DeleteChatMessage(ctx context.Context, id int32) error { _, err := q.db.ExecContext(ctx, deleteChatMessage, id) return err } const deleteChatMessageByUUID = `-- name: DeleteChatMessageByUUID :exec UPDATE chat_message SET is_deleted = true, updated_at = now() WHERE uuid = $1 ` func (q *Queries) DeleteChatMessageByUUID(ctx context.Context, uuid string) error { _, err := q.db.ExecContext(ctx, deleteChatMessageByUUID, uuid) return err } const deleteChatMessagesBySesionUUID = `-- name: DeleteChatMessagesBySesionUUID :exec UPDATE chat_message SET is_deleted = true, updated_at = now() WHERE is_deleted = false and is_pin = false and chat_session_uuid = $1 ` func (q *Queries) DeleteChatMessagesBySesionUUID(ctx context.Context, chatSessionUuid string) error { _, err := q.db.ExecContext(ctx, deleteChatMessagesBySesionUUID, chatSessionUuid) return err } const getAllChatMessages = `-- name: GetAllChatMessages :many SELECT 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 FROM chat_message WHERE is_deleted = false ORDER BY id ` func (q *Queries) GetAllChatMessages(ctx context.Context) ([]ChatMessage, error) { rows, err := q.db.QueryContext(ctx, getAllChatMessages) if err != nil { return nil, err } defer rows.Close() var items []ChatMessage for rows.Next() { var i ChatMessage if err := rows.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, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getChatMessageByID = `-- name: GetChatMessageByID :one SELECT 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 FROM chat_message WHERE is_deleted = false and id = $1 ` func (q *Queries) GetChatMessageByID(ctx context.Context, id int32) (ChatMessage, error) { row := q.db.QueryRowContext(ctx, getChatMessageByID, id) 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 } const getChatMessageBySessionUUID = `-- name: GetChatMessageBySessionUUID :one SELECT cm.id, cm.uuid, cm.chat_session_uuid, cm.role, cm.content, cm.reasoning_content, cm.model, cm.llm_summary, cm.score, cm.user_id, cm.created_at, cm.updated_at, cm.created_by, cm.updated_by, cm.is_deleted, cm.is_pin, cm.token_count, cm.raw, cm.artifacts, cm.suggested_questions FROM chat_message cm INNER JOIN chat_session cs ON cm.chat_session_uuid = cs.uuid WHERE cm.is_deleted = false and cs.active = true and cs.uuid = $1 ORDER BY cm.id OFFSET $2 LIMIT $1 ` type GetChatMessageBySessionUUIDParams struct { Limit int32 `json:"limit"` Offset int32 `json:"offset"` } func (q *Queries) GetChatMessageBySessionUUID(ctx context.Context, arg GetChatMessageBySessionUUIDParams) (ChatMessage, error) { row := q.db.QueryRowContext(ctx, getChatMessageBySessionUUID, arg.Limit, arg.Offset) 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 } const getChatMessageByUUID = `-- name: GetChatMessageByUUID :one SELECT 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 FROM chat_message WHERE is_deleted = false and uuid = $1 ` // -- UUID ---- func (q *Queries) GetChatMessageByUUID(ctx context.Context, uuid string) (ChatMessage, error) { row := q.db.QueryRowContext(ctx, getChatMessageByUUID, uuid) 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 } const getChatMessagesBySessionUUID = `-- name: GetChatMessagesBySessionUUID :many SELECT cm.id, cm.uuid, cm.chat_session_uuid, cm.role, cm.content, cm.reasoning_content, cm.model, cm.llm_summary, cm.score, cm.user_id, cm.created_at, cm.updated_at, cm.created_by, cm.updated_by, cm.is_deleted, cm.is_pin, cm.token_count, cm.raw, cm.artifacts, cm.suggested_questions FROM chat_message cm INNER JOIN chat_session cs ON cm.chat_session_uuid = cs.uuid WHERE cm.is_deleted = false and cs.active = true and cs.uuid = $1 ORDER BY cm.id OFFSET $2 LIMIT $3 ` type GetChatMessagesBySessionUUIDParams struct { Uuid string `json:"uuid"` Offset int32 `json:"offset"` Limit int32 `json:"limit"` } func (q *Queries) GetChatMessagesBySessionUUID(ctx context.Context, arg GetChatMessagesBySessionUUIDParams) ([]ChatMessage, error) { rows, err := q.db.QueryContext(ctx, getChatMessagesBySessionUUID, arg.Uuid, arg.Offset, arg.Limit) if err != nil { return nil, err } defer rows.Close() var items []ChatMessage for rows.Next() { var i ChatMessage if err := rows.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, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getChatMessagesBySessionUUIDForAdmin = `-- name: GetChatMessagesBySessionUUIDForAdmin :many SELECT id, uuid, role, content, reasoning_content, model, token_count, user_id, created_at, updated_at FROM ( -- Include session prompts as the first messages SELECT cp.id, cp.uuid, cp.role, cp.content, ''::text as reasoning_content, cs.model, cp.token_count, cp.user_id, cp.created_at, cp.updated_at FROM chat_prompt cp INNER JOIN chat_session cs ON cp.chat_session_uuid = cs.uuid WHERE cp.chat_session_uuid = $1 AND cp.is_deleted = false AND cp.role = 'system' UNION ALL -- Include regular chat messages SELECT id, uuid, role, content, reasoning_content, model, token_count, user_id, created_at, updated_at FROM chat_message WHERE chat_session_uuid = $1 AND is_deleted = false ) combined_messages ORDER BY created_at ASC ` type GetChatMessagesBySessionUUIDForAdminRow struct { ID int32 `json:"id"` Uuid string `json:"uuid"` Role string `json:"role"` Content string `json:"content"` ReasoningContent string `json:"reasoningContent"` Model string `json:"model"` TokenCount int32 `json:"tokenCount"` UserID int32 `json:"userId"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } func (q *Queries) GetChatMessagesBySessionUUIDForAdmin(ctx context.Context, chatSessionUuid string) ([]GetChatMessagesBySessionUUIDForAdminRow, error) { rows, err := q.db.QueryContext(ctx, getChatMessagesBySessionUUIDForAdmin, chatSessionUuid) if err != nil { return nil, err } defer rows.Close() var items []GetChatMessagesBySessionUUIDForAdminRow for rows.Next() { var i GetChatMessagesBySessionUUIDForAdminRow if err := rows.Scan( &i.ID, &i.Uuid, &i.Role, &i.Content, &i.ReasoningContent, &i.Model, &i.TokenCount, &i.UserID, &i.CreatedAt, &i.UpdatedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getChatMessagesCount = `-- name: GetChatMessagesCount :one SELECT COUNT(*) FROM chat_message WHERE user_id = $1 AND created_at >= NOW() - INTERVAL '10 minutes' ` // Get total chat message count for user in last 10 minutes func (q *Queries) GetChatMessagesCount(ctx context.Context, userID int32) (int64, error) { row := q.db.QueryRowContext(ctx, getChatMessagesCount, userID) var count int64 err := row.Scan(&count) return count, err } const getChatMessagesCountByUserAndModel = `-- name: GetChatMessagesCountByUserAndModel :one SELECT COUNT(*) FROM chat_message cm JOIN chat_session cs ON (cm.chat_session_uuid = cs.uuid AND cs.user_id = cm.user_id) WHERE cm.user_id = $1 AND cs.model = $2 AND cm.created_at >= NOW() - INTERVAL '10 minutes' ` type GetChatMessagesCountByUserAndModelParams struct { UserID int32 `json:"userId"` Model string `json:"model"` } // Get total chat message count for user of model in last 10 minutes func (q *Queries) GetChatMessagesCountByUserAndModel(ctx context.Context, arg GetChatMessagesCountByUserAndModelParams) (int64, error) { row := q.db.QueryRowContext(ctx, getChatMessagesCountByUserAndModel, arg.UserID, arg.Model) var count int64 err := row.Scan(&count) return count, err } const getFirstMessageBySessionUUID = `-- name: GetFirstMessageBySessionUUID :one SELECT 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 FROM chat_message WHERE chat_session_uuid = $1 and is_deleted = false ORDER BY created_at LIMIT 1 ` func (q *Queries) GetFirstMessageBySessionUUID(ctx context.Context, chatSessionUuid string) (ChatMessage, error) { row := q.db.QueryRowContext(ctx, getFirstMessageBySessionUUID, chatSessionUuid) 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 } const getLastNChatMessages = `-- name: GetLastNChatMessages :many SELECT 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 FROM chat_message WHERE chat_message.id in ( SELECT id FROM chat_message cm WHERE cm.chat_session_uuid = $3 and cm.is_deleted = false and cm.is_pin = true UNION ( SELECT id FROM chat_message cm WHERE cm.chat_session_uuid = $3 AND cm.id < (SELECT id FROM chat_message WHERE chat_message.uuid = $1) AND cm.is_deleted = false -- and cm.is_pin = false ORDER BY cm.created_at DESC LIMIT $2 ) ) ORDER BY created_at ` type GetLastNChatMessagesParams struct { Uuid string `json:"uuid"` Limit int32 `json:"limit"` ChatSessionUuid string `json:"chatSessionUuid"` } func (q *Queries) GetLastNChatMessages(ctx context.Context, arg GetLastNChatMessagesParams) ([]ChatMessage, error) { rows, err := q.db.QueryContext(ctx, getLastNChatMessages, arg.Uuid, arg.Limit, arg.ChatSessionUuid) if err != nil { return nil, err } defer rows.Close() var items []ChatMessage for rows.Next() { var i ChatMessage if err := rows.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, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getLatestMessagesBySessionUUID = `-- name: GetLatestMessagesBySessionUUID :many SELECT 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 FROM chat_message Where chat_message.id in ( SELECT chat_message.id FROM chat_message WHERE chat_message.chat_session_uuid = $1 and chat_message.is_deleted = false and chat_message.is_pin = true UNION ( SELECT chat_message.id FROM chat_message WHERE chat_message.chat_session_uuid = $1 and chat_message.is_deleted = false -- and chat_message.is_pin = false ORDER BY created_at DESC LIMIT $2 ) ) ORDER BY created_at ` type GetLatestMessagesBySessionUUIDParams struct { ChatSessionUuid string `json:"chatSessionUuid"` Limit int32 `json:"limit"` } func (q *Queries) GetLatestMessagesBySessionUUID(ctx context.Context, arg GetLatestMessagesBySessionUUIDParams) ([]ChatMessage, error) { rows, err := q.db.QueryContext(ctx, getLatestMessagesBySessionUUID, arg.ChatSessionUuid, arg.Limit) if err != nil { return nil, err } defer rows.Close() var items []ChatMessage for rows.Next() { var i ChatMessage if err := rows.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, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getLatestUsageTimeOfModel = `-- name: GetLatestUsageTimeOfModel :many SELECT model, MAX(created_at)::timestamp as latest_message_time, COUNT(*) as message_count FROM chat_message WHERE created_at >= NOW() - $1::text::INTERVAL AND is_deleted = false AND model != '' AND role = 'assistant' GROUP BY model ORDER BY latest_message_time DESC ` type GetLatestUsageTimeOfModelRow struct { Model string `json:"model"` LatestMessageTime time.Time `json:"latestMessageTime"` MessageCount int64 `json:"messageCount"` } func (q *Queries) GetLatestUsageTimeOfModel(ctx context.Context, timeInterval string) ([]GetLatestUsageTimeOfModelRow, error) { rows, err := q.db.QueryContext(ctx, getLatestUsageTimeOfModel, timeInterval) if err != nil { return nil, err } defer rows.Close() var items []GetLatestUsageTimeOfModelRow for rows.Next() { var i GetLatestUsageTimeOfModelRow if err := rows.Scan(&i.Model, &i.LatestMessageTime, &i.MessageCount); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const hasChatMessagePermission = `-- name: HasChatMessagePermission :one SELECT COUNT(*) > 0 as has_permission FROM chat_message cm INNER JOIN chat_session cs ON cm.chat_session_uuid = cs.uuid INNER JOIN auth_user au ON cs.user_id = au.id WHERE cm.is_deleted = false and cm.id = $1 AND (cs.user_id = $2 OR au.is_superuser) and cs.active = true ` type HasChatMessagePermissionParams struct { ID int32 `json:"id"` UserID int32 `json:"userId"` } func (q *Queries) HasChatMessagePermission(ctx context.Context, arg HasChatMessagePermissionParams) (bool, error) { row := q.db.QueryRowContext(ctx, hasChatMessagePermission, arg.ID, arg.UserID) var has_permission bool err := row.Scan(&has_permission) return has_permission, err } const updateChatMessage = `-- name: UpdateChatMessage :one UPDATE chat_message SET role = $2, content = $3, score = $4, user_id = $5, updated_by = $6, artifacts = $7, suggested_questions = $8, updated_at = now() WHERE id = $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 UpdateChatMessageParams struct { ID int32 `json:"id"` Role string `json:"role"` Content string `json:"content"` Score float64 `json:"score"` UserID int32 `json:"userId"` UpdatedBy int32 `json:"updatedBy"` Artifacts json.RawMessage `json:"artifacts"` SuggestedQuestions json.RawMessage `json:"suggestedQuestions"` } func (q *Queries) UpdateChatMessage(ctx context.Context, arg UpdateChatMessageParams) (ChatMessage, error) { row := q.db.QueryRowContext(ctx, updateChatMessage, arg.ID, arg.Role, arg.Content, arg.Score, arg.UserID, arg.UpdatedBy, arg.Artifacts, 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 } const updateChatMessageByUUID = `-- name: UpdateChatMessageByUUID :one UPDATE chat_message SET content = $2, is_pin = $3, token_count = $4, artifacts = $5, suggested_questions = $6, 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 UpdateChatMessageByUUIDParams struct { Uuid string `json:"uuid"` Content string `json:"content"` IsPin bool `json:"isPin"` TokenCount int32 `json:"tokenCount"` Artifacts json.RawMessage `json:"artifacts"` SuggestedQuestions json.RawMessage `json:"suggestedQuestions"` } func (q *Queries) UpdateChatMessageByUUID(ctx context.Context, arg UpdateChatMessageByUUIDParams) (ChatMessage, error) { row := q.db.QueryRowContext(ctx, updateChatMessageByUUID, arg.Uuid, arg.Content, arg.IsPin, arg.TokenCount, arg.Artifacts, 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 } const updateChatMessageContent = `-- name: UpdateChatMessageContent :exec UPDATE chat_message SET content = $2, updated_at = now(), token_count = $3 WHERE uuid = $1 ` type UpdateChatMessageContentParams struct { Uuid string `json:"uuid"` Content string `json:"content"` TokenCount int32 `json:"tokenCount"` } func (q *Queries) UpdateChatMessageContent(ctx context.Context, arg UpdateChatMessageContentParams) error { _, 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 } ================================================ FILE: api/sqlc_queries/chat_model.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: chat_model.sql package sqlc_queries import ( "context" ) const chatModelByID = `-- name: ChatModelByID :one SELECT id, name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, is_enable, api_type FROM chat_model WHERE id = $1 ` func (q *Queries) ChatModelByID(ctx context.Context, id int32) (ChatModel, error) { row := q.db.QueryRowContext(ctx, chatModelByID, id) var i ChatModel err := row.Scan( &i.ID, &i.Name, &i.Label, &i.IsDefault, &i.Url, &i.ApiAuthHeader, &i.ApiAuthKey, &i.UserID, &i.EnablePerModeRatelimit, &i.MaxToken, &i.DefaultToken, &i.OrderNumber, &i.HttpTimeOut, &i.IsEnable, &i.ApiType, ) return i, err } const chatModelByName = `-- name: ChatModelByName :one SELECT id, name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, is_enable, api_type FROM chat_model WHERE name = $1 ` func (q *Queries) ChatModelByName(ctx context.Context, name string) (ChatModel, error) { row := q.db.QueryRowContext(ctx, chatModelByName, name) var i ChatModel err := row.Scan( &i.ID, &i.Name, &i.Label, &i.IsDefault, &i.Url, &i.ApiAuthHeader, &i.ApiAuthKey, &i.UserID, &i.EnablePerModeRatelimit, &i.MaxToken, &i.DefaultToken, &i.OrderNumber, &i.HttpTimeOut, &i.IsEnable, &i.ApiType, ) return i, err } const createChatModel = `-- name: CreateChatModel :one INSERT INTO chat_model (name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, api_type ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, is_enable, api_type ` type CreateChatModelParams struct { Name string `json:"name"` Label string `json:"label"` IsDefault bool `json:"isDefault"` Url string `json:"url"` ApiAuthHeader string `json:"apiAuthHeader"` ApiAuthKey string `json:"apiAuthKey"` UserID int32 `json:"userId"` EnablePerModeRatelimit bool `json:"enablePerModeRatelimit"` MaxToken int32 `json:"maxToken"` DefaultToken int32 `json:"defaultToken"` OrderNumber int32 `json:"orderNumber"` HttpTimeOut int32 `json:"httpTimeOut"` ApiType string `json:"apiType"` } func (q *Queries) CreateChatModel(ctx context.Context, arg CreateChatModelParams) (ChatModel, error) { row := q.db.QueryRowContext(ctx, createChatModel, arg.Name, arg.Label, arg.IsDefault, arg.Url, arg.ApiAuthHeader, arg.ApiAuthKey, arg.UserID, arg.EnablePerModeRatelimit, arg.MaxToken, arg.DefaultToken, arg.OrderNumber, arg.HttpTimeOut, arg.ApiType, ) var i ChatModel err := row.Scan( &i.ID, &i.Name, &i.Label, &i.IsDefault, &i.Url, &i.ApiAuthHeader, &i.ApiAuthKey, &i.UserID, &i.EnablePerModeRatelimit, &i.MaxToken, &i.DefaultToken, &i.OrderNumber, &i.HttpTimeOut, &i.IsEnable, &i.ApiType, ) return i, err } const deleteChatModel = `-- name: DeleteChatModel :exec DELETE FROM chat_model WHERE id = $1 and user_id = $2 ` type DeleteChatModelParams struct { ID int32 `json:"id"` UserID int32 `json:"userId"` } func (q *Queries) DeleteChatModel(ctx context.Context, arg DeleteChatModelParams) error { _, err := q.db.ExecContext(ctx, deleteChatModel, arg.ID, arg.UserID) return err } const getDefaultChatModel = `-- name: GetDefaultChatModel :one SELECT id, name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, is_enable, api_type FROM chat_model WHERE is_default = true and user_id in (select id from auth_user where is_superuser = true) ORDER BY order_number, id LIMIT 1 ` func (q *Queries) GetDefaultChatModel(ctx context.Context) (ChatModel, error) { row := q.db.QueryRowContext(ctx, getDefaultChatModel) var i ChatModel err := row.Scan( &i.ID, &i.Name, &i.Label, &i.IsDefault, &i.Url, &i.ApiAuthHeader, &i.ApiAuthKey, &i.UserID, &i.EnablePerModeRatelimit, &i.MaxToken, &i.DefaultToken, &i.OrderNumber, &i.HttpTimeOut, &i.IsEnable, &i.ApiType, ) return i, err } const listChatModels = `-- name: ListChatModels :many SELECT id, name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, is_enable, api_type FROM chat_model ORDER BY order_number ` func (q *Queries) ListChatModels(ctx context.Context) ([]ChatModel, error) { rows, err := q.db.QueryContext(ctx, listChatModels) if err != nil { return nil, err } defer rows.Close() var items []ChatModel for rows.Next() { var i ChatModel if err := rows.Scan( &i.ID, &i.Name, &i.Label, &i.IsDefault, &i.Url, &i.ApiAuthHeader, &i.ApiAuthKey, &i.UserID, &i.EnablePerModeRatelimit, &i.MaxToken, &i.DefaultToken, &i.OrderNumber, &i.HttpTimeOut, &i.IsEnable, &i.ApiType, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listSystemChatModels = `-- name: ListSystemChatModels :many SELECT id, name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, is_enable, api_type FROM chat_model where user_id in (select id from auth_user where is_superuser = true) ORDER BY order_number, id desc ` func (q *Queries) ListSystemChatModels(ctx context.Context) ([]ChatModel, error) { rows, err := q.db.QueryContext(ctx, listSystemChatModels) if err != nil { return nil, err } defer rows.Close() var items []ChatModel for rows.Next() { var i ChatModel if err := rows.Scan( &i.ID, &i.Name, &i.Label, &i.IsDefault, &i.Url, &i.ApiAuthHeader, &i.ApiAuthKey, &i.UserID, &i.EnablePerModeRatelimit, &i.MaxToken, &i.DefaultToken, &i.OrderNumber, &i.HttpTimeOut, &i.IsEnable, &i.ApiType, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const updateChatModel = `-- name: UpdateChatModel :one UPDATE chat_model SET name = $2, label = $3, is_default = $4, url = $5, api_auth_header = $6, api_auth_key = $7, enable_per_mode_ratelimit = $9, max_token = $10, default_token = $11, order_number = $12, http_time_out = $13, is_enable = $14, api_type = $15 WHERE id = $1 and user_id = $8 RETURNING id, name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, is_enable, api_type ` type UpdateChatModelParams struct { ID int32 `json:"id"` Name string `json:"name"` Label string `json:"label"` IsDefault bool `json:"isDefault"` Url string `json:"url"` ApiAuthHeader string `json:"apiAuthHeader"` ApiAuthKey string `json:"apiAuthKey"` UserID int32 `json:"userId"` EnablePerModeRatelimit bool `json:"enablePerModeRatelimit"` MaxToken int32 `json:"maxToken"` DefaultToken int32 `json:"defaultToken"` OrderNumber int32 `json:"orderNumber"` HttpTimeOut int32 `json:"httpTimeOut"` IsEnable bool `json:"isEnable"` ApiType string `json:"apiType"` } func (q *Queries) UpdateChatModel(ctx context.Context, arg UpdateChatModelParams) (ChatModel, error) { row := q.db.QueryRowContext(ctx, updateChatModel, arg.ID, arg.Name, arg.Label, arg.IsDefault, arg.Url, arg.ApiAuthHeader, arg.ApiAuthKey, arg.UserID, arg.EnablePerModeRatelimit, arg.MaxToken, arg.DefaultToken, arg.OrderNumber, arg.HttpTimeOut, arg.IsEnable, arg.ApiType, ) var i ChatModel err := row.Scan( &i.ID, &i.Name, &i.Label, &i.IsDefault, &i.Url, &i.ApiAuthHeader, &i.ApiAuthKey, &i.UserID, &i.EnablePerModeRatelimit, &i.MaxToken, &i.DefaultToken, &i.OrderNumber, &i.HttpTimeOut, &i.IsEnable, &i.ApiType, ) return i, err } const updateChatModelKey = `-- name: UpdateChatModelKey :one UPDATE chat_model SET api_auth_key = $2 WHERE id = $1 RETURNING id, name, label, is_default, url, api_auth_header, api_auth_key, user_id, enable_per_mode_ratelimit, max_token, default_token, order_number, http_time_out, is_enable, api_type ` type UpdateChatModelKeyParams struct { ID int32 `json:"id"` ApiAuthKey string `json:"apiAuthKey"` } func (q *Queries) UpdateChatModelKey(ctx context.Context, arg UpdateChatModelKeyParams) (ChatModel, error) { row := q.db.QueryRowContext(ctx, updateChatModelKey, arg.ID, arg.ApiAuthKey) var i ChatModel err := row.Scan( &i.ID, &i.Name, &i.Label, &i.IsDefault, &i.Url, &i.ApiAuthHeader, &i.ApiAuthKey, &i.UserID, &i.EnablePerModeRatelimit, &i.MaxToken, &i.DefaultToken, &i.OrderNumber, &i.HttpTimeOut, &i.IsEnable, &i.ApiType, ) return i, err } ================================================ FILE: api/sqlc_queries/chat_prompt.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: chat_prompt.sql package sqlc_queries import ( "context" ) const createChatPrompt = `-- name: CreateChatPrompt :one INSERT INTO chat_prompt (uuid, chat_session_uuid, role, content, token_count, user_id, created_by, updated_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count ` type CreateChatPromptParams struct { Uuid string `json:"uuid"` ChatSessionUuid string `json:"chatSessionUuid"` Role string `json:"role"` Content string `json:"content"` TokenCount int32 `json:"tokenCount"` UserID int32 `json:"userId"` CreatedBy int32 `json:"createdBy"` UpdatedBy int32 `json:"updatedBy"` } func (q *Queries) CreateChatPrompt(ctx context.Context, arg CreateChatPromptParams) (ChatPrompt, error) { row := q.db.QueryRowContext(ctx, createChatPrompt, arg.Uuid, arg.ChatSessionUuid, arg.Role, arg.Content, arg.TokenCount, arg.UserID, arg.CreatedBy, arg.UpdatedBy, ) var i ChatPrompt err := row.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ) return i, err } const deleteChatPrompt = `-- name: DeleteChatPrompt :exec UPDATE chat_prompt SET is_deleted = true, updated_at = now() WHERE id = $1 ` func (q *Queries) DeleteChatPrompt(ctx context.Context, id int32) error { _, err := q.db.ExecContext(ctx, deleteChatPrompt, id) return err } const deleteChatPromptByUUID = `-- name: DeleteChatPromptByUUID :exec UPDATE chat_prompt SET is_deleted = true, updated_at = now() WHERE uuid = $1 ` func (q *Queries) DeleteChatPromptByUUID(ctx context.Context, uuid string) error { _, err := q.db.ExecContext(ctx, deleteChatPromptByUUID, uuid) return err } const getAllChatPrompts = `-- name: GetAllChatPrompts :many SELECT id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count FROM chat_prompt WHERE is_deleted = false ORDER BY id ` func (q *Queries) GetAllChatPrompts(ctx context.Context) ([]ChatPrompt, error) { rows, err := q.db.QueryContext(ctx, getAllChatPrompts) if err != nil { return nil, err } defer rows.Close() var items []ChatPrompt for rows.Next() { var i ChatPrompt if err := rows.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getChatPromptByID = `-- name: GetChatPromptByID :one SELECT id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count FROM chat_prompt WHERE is_deleted = false and id = $1 ` func (q *Queries) GetChatPromptByID(ctx context.Context, id int32) (ChatPrompt, error) { row := q.db.QueryRowContext(ctx, getChatPromptByID, id) var i ChatPrompt err := row.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ) return i, err } const getChatPromptByUUID = `-- name: GetChatPromptByUUID :one SELECT id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count FROM chat_prompt WHERE uuid = $1 ` func (q *Queries) GetChatPromptByUUID(ctx context.Context, uuid string) (ChatPrompt, error) { row := q.db.QueryRowContext(ctx, getChatPromptByUUID, uuid) var i ChatPrompt err := row.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ) return i, err } const getChatPromptsBySessionUUID = `-- name: GetChatPromptsBySessionUUID :many SELECT id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count FROM chat_prompt WHERE chat_session_uuid = $1 and is_deleted = false ORDER BY id ` func (q *Queries) GetChatPromptsBySessionUUID(ctx context.Context, chatSessionUuid string) ([]ChatPrompt, error) { rows, err := q.db.QueryContext(ctx, getChatPromptsBySessionUUID, chatSessionUuid) if err != nil { return nil, err } defer rows.Close() var items []ChatPrompt for rows.Next() { var i ChatPrompt if err := rows.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getChatPromptsByUserID = `-- name: GetChatPromptsByUserID :many SELECT id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count FROM chat_prompt WHERE user_id = $1 and is_deleted = false ORDER BY id ` func (q *Queries) GetChatPromptsByUserID(ctx context.Context, userID int32) ([]ChatPrompt, error) { rows, err := q.db.QueryContext(ctx, getChatPromptsByUserID, userID) if err != nil { return nil, err } defer rows.Close() var items []ChatPrompt for rows.Next() { var i ChatPrompt if err := rows.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getChatPromptsBysession_uuid = `-- name: GetChatPromptsBysession_uuid :many SELECT id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count FROM chat_prompt WHERE chat_session_uuid = $1 and is_deleted = false ORDER BY id ` func (q *Queries) GetChatPromptsBysession_uuid(ctx context.Context, chatSessionUuid string) ([]ChatPrompt, error) { rows, err := q.db.QueryContext(ctx, getChatPromptsBysession_uuid, chatSessionUuid) if err != nil { return nil, err } defer rows.Close() var items []ChatPrompt for rows.Next() { var i ChatPrompt if err := rows.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getOneChatPromptBySessionUUID = `-- name: GetOneChatPromptBySessionUUID :one SELECT id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count FROM chat_prompt WHERE chat_session_uuid = $1 and is_deleted = false ORDER BY id LIMIT 1 ` func (q *Queries) GetOneChatPromptBySessionUUID(ctx context.Context, chatSessionUuid string) (ChatPrompt, error) { row := q.db.QueryRowContext(ctx, getOneChatPromptBySessionUUID, chatSessionUuid) var i ChatPrompt err := row.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ) return i, err } const hasChatPromptPermission = `-- name: HasChatPromptPermission :one SELECT COUNT(*) > 0 as has_permission FROM chat_prompt cp INNER JOIN auth_user au ON cp.user_id = au.id WHERE cp.id = $1 AND (cp.user_id = $2 OR au.is_superuser) AND cp.is_deleted = false ` type HasChatPromptPermissionParams struct { ID int32 `json:"id"` UserID int32 `json:"userId"` } func (q *Queries) HasChatPromptPermission(ctx context.Context, arg HasChatPromptPermissionParams) (bool, error) { row := q.db.QueryRowContext(ctx, hasChatPromptPermission, arg.ID, arg.UserID) var has_permission bool err := row.Scan(&has_permission) return has_permission, err } const updateChatPrompt = `-- name: UpdateChatPrompt :one UPDATE chat_prompt SET chat_session_uuid = $2, role = $3, content = $4, score = $5, user_id = $6, updated_at = now(), updated_by = $7 WHERE id = $1 RETURNING id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count ` type UpdateChatPromptParams struct { ID int32 `json:"id"` ChatSessionUuid string `json:"chatSessionUuid"` Role string `json:"role"` Content string `json:"content"` Score float64 `json:"score"` UserID int32 `json:"userId"` UpdatedBy int32 `json:"updatedBy"` } func (q *Queries) UpdateChatPrompt(ctx context.Context, arg UpdateChatPromptParams) (ChatPrompt, error) { row := q.db.QueryRowContext(ctx, updateChatPrompt, arg.ID, arg.ChatSessionUuid, arg.Role, arg.Content, arg.Score, arg.UserID, arg.UpdatedBy, ) var i ChatPrompt err := row.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ) return i, err } const updateChatPromptByUUID = `-- name: UpdateChatPromptByUUID :one UPDATE chat_prompt SET content = $2, token_count = $3, updated_at = now() WHERE uuid = $1 and is_deleted = false RETURNING id, uuid, chat_session_uuid, role, content, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, token_count ` type UpdateChatPromptByUUIDParams struct { Uuid string `json:"uuid"` Content string `json:"content"` TokenCount int32 `json:"tokenCount"` } func (q *Queries) UpdateChatPromptByUUID(ctx context.Context, arg UpdateChatPromptByUUIDParams) (ChatPrompt, error) { row := q.db.QueryRowContext(ctx, updateChatPromptByUUID, arg.Uuid, arg.Content, arg.TokenCount) var i ChatPrompt err := row.Scan( &i.ID, &i.Uuid, &i.ChatSessionUuid, &i.Role, &i.Content, &i.Score, &i.UserID, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, &i.IsDeleted, &i.TokenCount, ) return i, err } ================================================ FILE: api/sqlc_queries/chat_session.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: chat_session.sql package sqlc_queries import ( "context" "database/sql" "time" ) const createChatSession = `-- name: CreateChatSession :one INSERT INTO chat_session (user_id, topic, max_length, uuid, model) VALUES ($1, $2, $3, $4, $5) RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type CreateChatSessionParams struct { UserID int32 `json:"userId"` Topic string `json:"topic"` MaxLength int32 `json:"maxLength"` Uuid string `json:"uuid"` Model string `json:"model"` } func (q *Queries) CreateChatSession(ctx context.Context, arg CreateChatSessionParams) (ChatSession, error) { row := q.db.QueryRowContext(ctx, createChatSession, arg.UserID, arg.Topic, arg.MaxLength, arg.Uuid, arg.Model, ) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const createChatSessionByUUID = `-- name: CreateChatSessionByUUID :one INSERT INTO chat_session (user_id, uuid, topic, created_at, active, max_length, model) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type CreateChatSessionByUUIDParams struct { UserID int32 `json:"userId"` Uuid string `json:"uuid"` Topic string `json:"topic"` CreatedAt time.Time `json:"createdAt"` Active bool `json:"active"` MaxLength int32 `json:"maxLength"` Model string `json:"model"` } func (q *Queries) CreateChatSessionByUUID(ctx context.Context, arg CreateChatSessionByUUIDParams) (ChatSession, error) { row := q.db.QueryRowContext(ctx, createChatSessionByUUID, arg.UserID, arg.Uuid, arg.Topic, arg.CreatedAt, arg.Active, arg.MaxLength, arg.Model, ) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const createChatSessionInWorkspace = `-- name: CreateChatSessionInWorkspace :one INSERT INTO chat_session (user_id, uuid, topic, created_at, active, max_length, model, workspace_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type CreateChatSessionInWorkspaceParams struct { UserID int32 `json:"userId"` Uuid string `json:"uuid"` Topic string `json:"topic"` CreatedAt time.Time `json:"createdAt"` Active bool `json:"active"` MaxLength int32 `json:"maxLength"` Model string `json:"model"` WorkspaceID sql.NullInt32 `json:"workspaceId"` } func (q *Queries) CreateChatSessionInWorkspace(ctx context.Context, arg CreateChatSessionInWorkspaceParams) (ChatSession, error) { row := q.db.QueryRowContext(ctx, createChatSessionInWorkspace, arg.UserID, arg.Uuid, arg.Topic, arg.CreatedAt, arg.Active, arg.MaxLength, arg.Model, arg.WorkspaceID, ) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const createOrUpdateChatSessionByUUID = `-- name: CreateOrUpdateChatSessionByUUID :one INSERT INTO chat_session(uuid, user_id, topic, max_length, temperature, model, max_tokens, top_p, n, debug, summarize_mode, workspace_id, explore_mode, artifact_enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (uuid) DO UPDATE SET max_length = EXCLUDED.max_length, debug = EXCLUDED.debug, max_tokens = EXCLUDED.max_tokens, temperature = EXCLUDED.temperature, top_p = EXCLUDED.top_p, n= EXCLUDED.n, model = EXCLUDED.model, summarize_mode = EXCLUDED.summarize_mode, artifact_enabled = EXCLUDED.artifact_enabled, workspace_id = CASE WHEN EXCLUDED.workspace_id IS NOT NULL THEN EXCLUDED.workspace_id ELSE chat_session.workspace_id END, topic = CASE WHEN chat_session.topic IS NULL THEN EXCLUDED.topic ELSE chat_session.topic END, explore_mode = EXCLUDED.explore_mode, updated_at = now() returning id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type CreateOrUpdateChatSessionByUUIDParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` Topic string `json:"topic"` MaxLength int32 `json:"maxLength"` Temperature float64 `json:"temperature"` Model string `json:"model"` MaxTokens int32 `json:"maxTokens"` TopP float64 `json:"topP"` N int32 `json:"n"` Debug bool `json:"debug"` SummarizeMode bool `json:"summarizeMode"` WorkspaceID sql.NullInt32 `json:"workspaceId"` ExploreMode bool `json:"exploreMode"` ArtifactEnabled bool `json:"artifactEnabled"` } func (q *Queries) CreateOrUpdateChatSessionByUUID(ctx context.Context, arg CreateOrUpdateChatSessionByUUIDParams) (ChatSession, error) { row := q.db.QueryRowContext(ctx, createOrUpdateChatSessionByUUID, arg.Uuid, arg.UserID, arg.Topic, arg.MaxLength, arg.Temperature, arg.Model, arg.MaxTokens, arg.TopP, arg.N, arg.Debug, arg.SummarizeMode, arg.WorkspaceID, arg.ExploreMode, arg.ArtifactEnabled, ) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const deleteChatSession = `-- name: DeleteChatSession :exec DELETE FROM chat_session WHERE id = $1 ` func (q *Queries) DeleteChatSession(ctx context.Context, id int32) error { _, err := q.db.ExecContext(ctx, deleteChatSession, id) return err } const deleteChatSessionByUUID = `-- name: DeleteChatSessionByUUID :exec update chat_session set active = false WHERE uuid = $1 returning id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` func (q *Queries) DeleteChatSessionByUUID(ctx context.Context, uuid string) error { _, err := q.db.ExecContext(ctx, deleteChatSessionByUUID, uuid) return err } const getAllChatSessions = `-- name: GetAllChatSessions :many SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session where active = true ORDER BY id ` func (q *Queries) GetAllChatSessions(ctx context.Context) ([]ChatSession, error) { rows, err := q.db.QueryContext(ctx, getAllChatSessions) if err != nil { return nil, err } defer rows.Close() var items []ChatSession for rows.Next() { var i ChatSession if err := rows.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getChatSessionByID = `-- name: GetChatSessionByID :one SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session WHERE id = $1 ` func (q *Queries) GetChatSessionByID(ctx context.Context, id int32) (ChatSession, error) { row := q.db.QueryRowContext(ctx, getChatSessionByID, id) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const getChatSessionByUUID = `-- name: GetChatSessionByUUID :one SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session WHERE active = true and uuid = $1 order by updated_at ` func (q *Queries) GetChatSessionByUUID(ctx context.Context, uuid string) (ChatSession, error) { row := q.db.QueryRowContext(ctx, getChatSessionByUUID, uuid) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const getChatSessionByUUIDWithInActive = `-- name: GetChatSessionByUUIDWithInActive :one SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session WHERE uuid = $1 order by updated_at ` func (q *Queries) GetChatSessionByUUIDWithInActive(ctx context.Context, uuid string) (ChatSession, error) { row := q.db.QueryRowContext(ctx, getChatSessionByUUIDWithInActive, uuid) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const getChatSessionsByUserID = `-- name: GetChatSessionsByUserID :many SELECT cs.id, cs.user_id, cs.uuid, cs.topic, cs.created_at, cs.updated_at, cs.active, cs.model, cs.max_length, cs.temperature, cs.top_p, cs.max_tokens, cs.n, cs.summarize_mode, cs.workspace_id, cs.artifact_enabled, cs.debug, cs.explore_mode FROM chat_session cs LEFT JOIN ( SELECT chat_session_uuid, MAX(created_at) AS latest_message_time FROM chat_message GROUP BY chat_session_uuid ) cm ON cs.uuid = cm.chat_session_uuid WHERE cs.user_id = $1 AND cs.active = true ORDER BY cm.latest_message_time DESC, cs.id DESC ` func (q *Queries) GetChatSessionsByUserID(ctx context.Context, userID int32) ([]ChatSession, error) { rows, err := q.db.QueryContext(ctx, getChatSessionsByUserID, userID) if err != nil { return nil, err } defer rows.Close() var items []ChatSession for rows.Next() { var i ChatSession if err := rows.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getSessionsByWorkspaceID = `-- name: GetSessionsByWorkspaceID :many SELECT cs.id, cs.user_id, cs.uuid, cs.topic, cs.created_at, cs.updated_at, cs.active, cs.model, cs.max_length, cs.temperature, cs.top_p, cs.max_tokens, cs.n, cs.summarize_mode, cs.workspace_id, cs.artifact_enabled, cs.debug, cs.explore_mode FROM chat_session cs LEFT JOIN ( SELECT chat_session_uuid, MAX(created_at) AS latest_message_time FROM chat_message GROUP BY chat_session_uuid ) cm ON cs.uuid = cm.chat_session_uuid WHERE cs.workspace_id = $1 AND cs.active = true ORDER BY cm.latest_message_time DESC, cs.id DESC ` func (q *Queries) GetSessionsByWorkspaceID(ctx context.Context, workspaceID sql.NullInt32) ([]ChatSession, error) { rows, err := q.db.QueryContext(ctx, getSessionsByWorkspaceID, workspaceID) if err != nil { return nil, err } defer rows.Close() var items []ChatSession for rows.Next() { var i ChatSession if err := rows.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getSessionsGroupedByWorkspace = `-- name: GetSessionsGroupedByWorkspace :many SELECT cs.id, cs.user_id, cs.uuid, cs.topic, cs.created_at, cs.updated_at, cs.active, cs.model, cs.max_length, cs.temperature, cs.top_p, cs.max_tokens, cs.n, cs.summarize_mode, cs.workspace_id, cs.artifact_enabled, cs.debug, cs.explore_mode, w.uuid as workspace_uuid, w.name as workspace_name, w.color as workspace_color, w.icon as workspace_icon FROM chat_session cs LEFT JOIN chat_workspace w ON cs.workspace_id = w.id LEFT JOIN ( SELECT chat_session_uuid, MAX(created_at) AS latest_message_time FROM chat_message GROUP BY chat_session_uuid ) cm ON cs.uuid = cm.chat_session_uuid WHERE cs.user_id = $1 AND cs.active = true ORDER BY w.order_position ASC, cm.latest_message_time DESC, cs.id DESC ` type GetSessionsGroupedByWorkspaceRow struct { ID int32 `json:"id"` UserID int32 `json:"userId"` Uuid string `json:"uuid"` Topic string `json:"topic"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` Active bool `json:"active"` Model string `json:"model"` MaxLength int32 `json:"maxLength"` Temperature float64 `json:"temperature"` TopP float64 `json:"topP"` MaxTokens int32 `json:"maxTokens"` N int32 `json:"n"` SummarizeMode bool `json:"summarizeMode"` WorkspaceID sql.NullInt32 `json:"workspaceId"` ArtifactEnabled bool `json:"artifactEnabled"` Debug bool `json:"debug"` ExploreMode bool `json:"exploreMode"` WorkspaceUuid sql.NullString `json:"workspaceUuid"` WorkspaceName sql.NullString `json:"workspaceName"` WorkspaceColor sql.NullString `json:"workspaceColor"` WorkspaceIcon sql.NullString `json:"workspaceIcon"` } func (q *Queries) GetSessionsGroupedByWorkspace(ctx context.Context, userID int32) ([]GetSessionsGroupedByWorkspaceRow, error) { rows, err := q.db.QueryContext(ctx, getSessionsGroupedByWorkspace, userID) if err != nil { return nil, err } defer rows.Close() var items []GetSessionsGroupedByWorkspaceRow for rows.Next() { var i GetSessionsGroupedByWorkspaceRow if err := rows.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, &i.WorkspaceUuid, &i.WorkspaceName, &i.WorkspaceColor, &i.WorkspaceIcon, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getSessionsWithoutWorkspace = `-- name: GetSessionsWithoutWorkspace :many SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session WHERE user_id = $1 AND workspace_id IS NULL AND active = true ` func (q *Queries) GetSessionsWithoutWorkspace(ctx context.Context, userID int32) ([]ChatSession, error) { rows, err := q.db.QueryContext(ctx, getSessionsWithoutWorkspace, userID) if err != nil { return nil, err } defer rows.Close() var items []ChatSession for rows.Next() { var i ChatSession if err := rows.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const hasChatSessionPermission = `-- name: HasChatSessionPermission :one SELECT COUNT(*) > 0 as has_permission FROM chat_session cs INNER JOIN auth_user au ON cs.user_id = au.id WHERE cs.id = $1 AND (cs.user_id = $2 OR au.is_superuser) ` type HasChatSessionPermissionParams struct { ID int32 `json:"id"` UserID int32 `json:"userId"` } // SELECT cs.* // FROM chat_session cs // WHERE cs.user_id = $1 and cs.active = true // ORDER BY cs.updated_at DESC; func (q *Queries) HasChatSessionPermission(ctx context.Context, arg HasChatSessionPermissionParams) (bool, error) { row := q.db.QueryRowContext(ctx, hasChatSessionPermission, arg.ID, arg.UserID) var has_permission bool err := row.Scan(&has_permission) return has_permission, err } const migrateSessionsToDefaultWorkspace = `-- name: MigrateSessionsToDefaultWorkspace :exec UPDATE chat_session SET workspace_id = $2 WHERE user_id = $1 AND workspace_id IS NULL ` type MigrateSessionsToDefaultWorkspaceParams struct { UserID int32 `json:"userId"` WorkspaceID sql.NullInt32 `json:"workspaceId"` } func (q *Queries) MigrateSessionsToDefaultWorkspace(ctx context.Context, arg MigrateSessionsToDefaultWorkspaceParams) error { _, err := q.db.ExecContext(ctx, migrateSessionsToDefaultWorkspace, arg.UserID, arg.WorkspaceID) return err } const updateChatSession = `-- name: UpdateChatSession :one UPDATE chat_session SET user_id = $2, topic = $3, updated_at = now(), active = $4 WHERE id = $1 RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateChatSessionParams struct { ID int32 `json:"id"` UserID int32 `json:"userId"` Topic string `json:"topic"` Active bool `json:"active"` } func (q *Queries) UpdateChatSession(ctx context.Context, arg UpdateChatSessionParams) (ChatSession, error) { row := q.db.QueryRowContext(ctx, updateChatSession, arg.ID, arg.UserID, arg.Topic, arg.Active, ) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const updateChatSessionByUUID = `-- name: UpdateChatSessionByUUID :one UPDATE chat_session SET user_id = $2, topic = $3, updated_at = now() WHERE uuid = $1 RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateChatSessionByUUIDParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` Topic string `json:"topic"` } func (q *Queries) UpdateChatSessionByUUID(ctx context.Context, arg UpdateChatSessionByUUIDParams) (ChatSession, error) { row := q.db.QueryRowContext(ctx, updateChatSessionByUUID, arg.Uuid, arg.UserID, arg.Topic) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const updateChatSessionTopicByUUID = `-- name: UpdateChatSessionTopicByUUID :one INSERT INTO chat_session(uuid, user_id, topic) VALUES ($1, $2, $3) ON CONFLICT (uuid) DO UPDATE SET topic = EXCLUDED.topic, updated_at = now() returning id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateChatSessionTopicByUUIDParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` Topic string `json:"topic"` } func (q *Queries) UpdateChatSessionTopicByUUID(ctx context.Context, arg UpdateChatSessionTopicByUUIDParams) (ChatSession, error) { row := q.db.QueryRowContext(ctx, updateChatSessionTopicByUUID, arg.Uuid, arg.UserID, arg.Topic) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const updateSessionMaxLength = `-- name: UpdateSessionMaxLength :one UPDATE chat_session SET max_length = $2, updated_at = now() WHERE uuid = $1 RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateSessionMaxLengthParams struct { Uuid string `json:"uuid"` MaxLength int32 `json:"maxLength"` } func (q *Queries) UpdateSessionMaxLength(ctx context.Context, arg UpdateSessionMaxLengthParams) (ChatSession, error) { row := q.db.QueryRowContext(ctx, updateSessionMaxLength, arg.Uuid, arg.MaxLength) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } const updateSessionWorkspace = `-- name: UpdateSessionWorkspace :one UPDATE chat_session SET workspace_id = $2, updated_at = now() WHERE uuid = $1 RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateSessionWorkspaceParams struct { Uuid string `json:"uuid"` WorkspaceID sql.NullInt32 `json:"workspaceId"` } func (q *Queries) UpdateSessionWorkspace(ctx context.Context, arg UpdateSessionWorkspaceParams) (ChatSession, error) { row := q.db.QueryRowContext(ctx, updateSessionWorkspace, arg.Uuid, arg.WorkspaceID) var i ChatSession err := row.Scan( &i.ID, &i.UserID, &i.Uuid, &i.Topic, &i.CreatedAt, &i.UpdatedAt, &i.Active, &i.Model, &i.MaxLength, &i.Temperature, &i.TopP, &i.MaxTokens, &i.N, &i.SummarizeMode, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, &i.ExploreMode, ) return i, err } ================================================ FILE: api/sqlc_queries/chat_snapshot.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: chat_snapshot.sql package sqlc_queries import ( "context" "encoding/json" "time" ) const chatSnapshotByID = `-- name: ChatSnapshotByID :one SELECT id, typ, uuid, user_id, title, summary, model, tags, session, conversation, created_at, text, search_vector FROM chat_snapshot WHERE id = $1 ` func (q *Queries) ChatSnapshotByID(ctx context.Context, id int32) (ChatSnapshot, error) { row := q.db.QueryRowContext(ctx, chatSnapshotByID, id) var i ChatSnapshot err := row.Scan( &i.ID, &i.Typ, &i.Uuid, &i.UserID, &i.Title, &i.Summary, &i.Model, &i.Tags, &i.Session, &i.Conversation, &i.CreatedAt, &i.Text, &i.SearchVector, ) return i, err } const chatSnapshotByUUID = `-- name: ChatSnapshotByUUID :one SELECT id, typ, uuid, user_id, title, summary, model, tags, session, conversation, created_at, text, search_vector FROM chat_snapshot WHERE uuid = $1 ` func (q *Queries) ChatSnapshotByUUID(ctx context.Context, uuid string) (ChatSnapshot, error) { row := q.db.QueryRowContext(ctx, chatSnapshotByUUID, uuid) var i ChatSnapshot err := row.Scan( &i.ID, &i.Typ, &i.Uuid, &i.UserID, &i.Title, &i.Summary, &i.Model, &i.Tags, &i.Session, &i.Conversation, &i.CreatedAt, &i.Text, &i.SearchVector, ) return i, err } const chatSnapshotByUserIdAndUuid = `-- name: ChatSnapshotByUserIdAndUuid :one SELECT id, typ, uuid, user_id, title, summary, model, tags, session, conversation, created_at, text, search_vector FROM chat_snapshot WHERE user_id = $1 AND uuid = $2 ` type ChatSnapshotByUserIdAndUuidParams struct { UserID int32 `json:"userId"` Uuid string `json:"uuid"` } func (q *Queries) ChatSnapshotByUserIdAndUuid(ctx context.Context, arg ChatSnapshotByUserIdAndUuidParams) (ChatSnapshot, error) { row := q.db.QueryRowContext(ctx, chatSnapshotByUserIdAndUuid, arg.UserID, arg.Uuid) var i ChatSnapshot err := row.Scan( &i.ID, &i.Typ, &i.Uuid, &i.UserID, &i.Title, &i.Summary, &i.Model, &i.Tags, &i.Session, &i.Conversation, &i.CreatedAt, &i.Text, &i.SearchVector, ) return i, err } const chatSnapshotCountByUserIDAndType = `-- name: ChatSnapshotCountByUserIDAndType :one SELECT COUNT(*) FROM chat_snapshot WHERE user_id = $1 AND ($2::text = '' OR typ = $2) ` type ChatSnapshotCountByUserIDAndTypeParams struct { UserID int32 `json:"userId"` Column2 string `json:"column2"` } func (q *Queries) ChatSnapshotCountByUserIDAndType(ctx context.Context, arg ChatSnapshotCountByUserIDAndTypeParams) (int64, error) { row := q.db.QueryRowContext(ctx, chatSnapshotCountByUserIDAndType, arg.UserID, arg.Column2) var count int64 err := row.Scan(&count) return count, err } const chatSnapshotMetaByUserID = `-- name: ChatSnapshotMetaByUserID :many SELECT uuid, title, summary, tags, created_at, typ FROM chat_snapshot WHERE user_id = $1 and typ = $2 order by created_at desc LIMIT $3 OFFSET $4 ` type ChatSnapshotMetaByUserIDParams struct { UserID int32 `json:"userId"` Typ string `json:"typ"` Limit int32 `json:"limit"` Offset int32 `json:"offset"` } type ChatSnapshotMetaByUserIDRow struct { Uuid string `json:"uuid"` Title string `json:"title"` Summary string `json:"summary"` Tags json.RawMessage `json:"tags"` CreatedAt time.Time `json:"createdAt"` Typ string `json:"typ"` } func (q *Queries) ChatSnapshotMetaByUserID(ctx context.Context, arg ChatSnapshotMetaByUserIDParams) ([]ChatSnapshotMetaByUserIDRow, error) { rows, err := q.db.QueryContext(ctx, chatSnapshotMetaByUserID, arg.UserID, arg.Typ, arg.Limit, arg.Offset, ) if err != nil { return nil, err } defer rows.Close() var items []ChatSnapshotMetaByUserIDRow for rows.Next() { var i ChatSnapshotMetaByUserIDRow if err := rows.Scan( &i.Uuid, &i.Title, &i.Summary, &i.Tags, &i.CreatedAt, &i.Typ, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const chatSnapshotSearch = `-- name: ChatSnapshotSearch :many SELECT uuid, title, ts_rank(search_vector, websearch_to_tsquery($2), 1) as rank FROM chat_snapshot WHERE search_vector @@ websearch_to_tsquery($2) AND user_id = $1 ORDER BY rank DESC LIMIT 20 ` type ChatSnapshotSearchParams struct { UserID int32 `json:"userId"` Search string `json:"search"` } type ChatSnapshotSearchRow struct { Uuid string `json:"uuid"` Title string `json:"title"` Rank float32 `json:"rank"` } func (q *Queries) ChatSnapshotSearch(ctx context.Context, arg ChatSnapshotSearchParams) ([]ChatSnapshotSearchRow, error) { rows, err := q.db.QueryContext(ctx, chatSnapshotSearch, arg.UserID, arg.Search) if err != nil { return nil, err } defer rows.Close() var items []ChatSnapshotSearchRow for rows.Next() { var i ChatSnapshotSearchRow if err := rows.Scan(&i.Uuid, &i.Title, &i.Rank); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const createChatBot = `-- name: CreateChatBot :one INSERT INTO chat_snapshot (uuid, user_id, typ, title, model, summary, tags, conversation ,session, text ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, typ, uuid, user_id, title, summary, model, tags, session, conversation, created_at, text, search_vector ` type CreateChatBotParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` Typ string `json:"typ"` Title string `json:"title"` Model string `json:"model"` Summary string `json:"summary"` Tags json.RawMessage `json:"tags"` Conversation json.RawMessage `json:"conversation"` Session json.RawMessage `json:"session"` Text string `json:"text"` } func (q *Queries) CreateChatBot(ctx context.Context, arg CreateChatBotParams) (ChatSnapshot, error) { row := q.db.QueryRowContext(ctx, createChatBot, arg.Uuid, arg.UserID, arg.Typ, arg.Title, arg.Model, arg.Summary, arg.Tags, arg.Conversation, arg.Session, arg.Text, ) var i ChatSnapshot err := row.Scan( &i.ID, &i.Typ, &i.Uuid, &i.UserID, &i.Title, &i.Summary, &i.Model, &i.Tags, &i.Session, &i.Conversation, &i.CreatedAt, &i.Text, &i.SearchVector, ) return i, err } const createChatSnapshot = `-- name: CreateChatSnapshot :one INSERT INTO chat_snapshot (uuid, user_id, title, model, summary, tags, conversation ,session, text ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, typ, uuid, user_id, title, summary, model, tags, session, conversation, created_at, text, search_vector ` type CreateChatSnapshotParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` Title string `json:"title"` Model string `json:"model"` Summary string `json:"summary"` Tags json.RawMessage `json:"tags"` Conversation json.RawMessage `json:"conversation"` Session json.RawMessage `json:"session"` Text string `json:"text"` } func (q *Queries) CreateChatSnapshot(ctx context.Context, arg CreateChatSnapshotParams) (ChatSnapshot, error) { row := q.db.QueryRowContext(ctx, createChatSnapshot, arg.Uuid, arg.UserID, arg.Title, arg.Model, arg.Summary, arg.Tags, arg.Conversation, arg.Session, arg.Text, ) var i ChatSnapshot err := row.Scan( &i.ID, &i.Typ, &i.Uuid, &i.UserID, &i.Title, &i.Summary, &i.Model, &i.Tags, &i.Session, &i.Conversation, &i.CreatedAt, &i.Text, &i.SearchVector, ) return i, err } const deleteChatSnapshot = `-- name: DeleteChatSnapshot :one DELETE FROM chat_snapshot WHERE uuid = $1 and user_id = $2 RETURNING id, typ, uuid, user_id, title, summary, model, tags, session, conversation, created_at, text, search_vector ` type DeleteChatSnapshotParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` } func (q *Queries) DeleteChatSnapshot(ctx context.Context, arg DeleteChatSnapshotParams) (ChatSnapshot, error) { row := q.db.QueryRowContext(ctx, deleteChatSnapshot, arg.Uuid, arg.UserID) var i ChatSnapshot err := row.Scan( &i.ID, &i.Typ, &i.Uuid, &i.UserID, &i.Title, &i.Summary, &i.Model, &i.Tags, &i.Session, &i.Conversation, &i.CreatedAt, &i.Text, &i.SearchVector, ) return i, err } const listChatSnapshots = `-- name: ListChatSnapshots :many SELECT id, typ, uuid, user_id, title, summary, model, tags, session, conversation, created_at, text, search_vector FROM chat_snapshot ORDER BY id ` func (q *Queries) ListChatSnapshots(ctx context.Context) ([]ChatSnapshot, error) { rows, err := q.db.QueryContext(ctx, listChatSnapshots) if err != nil { return nil, err } defer rows.Close() var items []ChatSnapshot for rows.Next() { var i ChatSnapshot if err := rows.Scan( &i.ID, &i.Typ, &i.Uuid, &i.UserID, &i.Title, &i.Summary, &i.Model, &i.Tags, &i.Session, &i.Conversation, &i.CreatedAt, &i.Text, &i.SearchVector, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const updateChatSnapshot = `-- name: UpdateChatSnapshot :one UPDATE chat_snapshot SET uuid = $2, user_id = $3, title = $4, summary = $5, tags = $6, conversation = $7, created_at = $8 WHERE id = $1 RETURNING id, typ, uuid, user_id, title, summary, model, tags, session, conversation, created_at, text, search_vector ` type UpdateChatSnapshotParams struct { ID int32 `json:"id"` Uuid string `json:"uuid"` UserID int32 `json:"userId"` Title string `json:"title"` Summary string `json:"summary"` Tags json.RawMessage `json:"tags"` Conversation json.RawMessage `json:"conversation"` CreatedAt time.Time `json:"createdAt"` } func (q *Queries) UpdateChatSnapshot(ctx context.Context, arg UpdateChatSnapshotParams) (ChatSnapshot, error) { row := q.db.QueryRowContext(ctx, updateChatSnapshot, arg.ID, arg.Uuid, arg.UserID, arg.Title, arg.Summary, arg.Tags, arg.Conversation, arg.CreatedAt, ) var i ChatSnapshot err := row.Scan( &i.ID, &i.Typ, &i.Uuid, &i.UserID, &i.Title, &i.Summary, &i.Model, &i.Tags, &i.Session, &i.Conversation, &i.CreatedAt, &i.Text, &i.SearchVector, ) return i, err } const updateChatSnapshotMetaByUUID = `-- name: UpdateChatSnapshotMetaByUUID :exec UPDATE chat_snapshot SET title = $2, summary = $3 WHERE uuid = $1 and user_id = $4 ` type UpdateChatSnapshotMetaByUUIDParams struct { Uuid string `json:"uuid"` Title string `json:"title"` Summary string `json:"summary"` UserID int32 `json:"userId"` } func (q *Queries) UpdateChatSnapshotMetaByUUID(ctx context.Context, arg UpdateChatSnapshotMetaByUUIDParams) error { _, err := q.db.ExecContext(ctx, updateChatSnapshotMetaByUUID, arg.Uuid, arg.Title, arg.Summary, arg.UserID, ) return err } ================================================ FILE: api/sqlc_queries/chat_workspace.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: chat_workspace.sql package sqlc_queries import ( "context" "time" ) const createDefaultWorkspace = `-- name: CreateDefaultWorkspace :one INSERT INTO chat_workspace (uuid, user_id, name, description, color, icon, is_default, order_position) VALUES ($1, $2, 'General', 'Default workspace for all conversations', '#6366f1', 'folder', true, 0) RETURNING id, uuid, user_id, name, description, color, icon, created_at, updated_at, is_default, order_position ` type CreateDefaultWorkspaceParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` } func (q *Queries) CreateDefaultWorkspace(ctx context.Context, arg CreateDefaultWorkspaceParams) (ChatWorkspace, error) { row := q.db.QueryRowContext(ctx, createDefaultWorkspace, arg.Uuid, arg.UserID) var i ChatWorkspace err := row.Scan( &i.ID, &i.Uuid, &i.UserID, &i.Name, &i.Description, &i.Color, &i.Icon, &i.CreatedAt, &i.UpdatedAt, &i.IsDefault, &i.OrderPosition, ) return i, err } const createWorkspace = `-- name: CreateWorkspace :one INSERT INTO chat_workspace (uuid, user_id, name, description, color, icon, is_default, order_position) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, uuid, user_id, name, description, color, icon, created_at, updated_at, is_default, order_position ` type CreateWorkspaceParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` IsDefault bool `json:"isDefault"` OrderPosition int32 `json:"orderPosition"` } func (q *Queries) CreateWorkspace(ctx context.Context, arg CreateWorkspaceParams) (ChatWorkspace, error) { row := q.db.QueryRowContext(ctx, createWorkspace, arg.Uuid, arg.UserID, arg.Name, arg.Description, arg.Color, arg.Icon, arg.IsDefault, arg.OrderPosition, ) var i ChatWorkspace err := row.Scan( &i.ID, &i.Uuid, &i.UserID, &i.Name, &i.Description, &i.Color, &i.Icon, &i.CreatedAt, &i.UpdatedAt, &i.IsDefault, &i.OrderPosition, ) return i, err } const deleteWorkspace = `-- name: DeleteWorkspace :exec DELETE FROM chat_workspace WHERE uuid = $1 ` func (q *Queries) DeleteWorkspace(ctx context.Context, uuid string) error { _, err := q.db.ExecContext(ctx, deleteWorkspace, uuid) return err } const getDefaultWorkspaceByUserID = `-- name: GetDefaultWorkspaceByUserID :one SELECT id, uuid, user_id, name, description, color, icon, created_at, updated_at, is_default, order_position FROM chat_workspace WHERE user_id = $1 AND is_default = true LIMIT 1 ` func (q *Queries) GetDefaultWorkspaceByUserID(ctx context.Context, userID int32) (ChatWorkspace, error) { row := q.db.QueryRowContext(ctx, getDefaultWorkspaceByUserID, userID) var i ChatWorkspace err := row.Scan( &i.ID, &i.Uuid, &i.UserID, &i.Name, &i.Description, &i.Color, &i.Icon, &i.CreatedAt, &i.UpdatedAt, &i.IsDefault, &i.OrderPosition, ) return i, err } const getWorkspaceByUUID = `-- name: GetWorkspaceByUUID :one SELECT id, uuid, user_id, name, description, color, icon, created_at, updated_at, is_default, order_position FROM chat_workspace WHERE uuid = $1 ` func (q *Queries) GetWorkspaceByUUID(ctx context.Context, uuid string) (ChatWorkspace, error) { row := q.db.QueryRowContext(ctx, getWorkspaceByUUID, uuid) var i ChatWorkspace err := row.Scan( &i.ID, &i.Uuid, &i.UserID, &i.Name, &i.Description, &i.Color, &i.Icon, &i.CreatedAt, &i.UpdatedAt, &i.IsDefault, &i.OrderPosition, ) return i, err } const getWorkspaceWithSessionCount = `-- name: GetWorkspaceWithSessionCount :many SELECT w.id, w.uuid, w.user_id, w.name, w.description, w.color, w.icon, w.created_at, w.updated_at, w.is_default, w.order_position, COUNT(cs.id) as session_count FROM chat_workspace w LEFT JOIN chat_session cs ON w.id = cs.workspace_id AND cs.active = true WHERE w.user_id = $1 GROUP BY w.id ORDER BY w.order_position ASC, w.created_at ASC ` type GetWorkspaceWithSessionCountRow struct { ID int32 `json:"id"` Uuid string `json:"uuid"` UserID int32 `json:"userId"` Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` IsDefault bool `json:"isDefault"` OrderPosition int32 `json:"orderPosition"` SessionCount int64 `json:"sessionCount"` } func (q *Queries) GetWorkspaceWithSessionCount(ctx context.Context, userID int32) ([]GetWorkspaceWithSessionCountRow, error) { rows, err := q.db.QueryContext(ctx, getWorkspaceWithSessionCount, userID) if err != nil { return nil, err } defer rows.Close() var items []GetWorkspaceWithSessionCountRow for rows.Next() { var i GetWorkspaceWithSessionCountRow if err := rows.Scan( &i.ID, &i.Uuid, &i.UserID, &i.Name, &i.Description, &i.Color, &i.Icon, &i.CreatedAt, &i.UpdatedAt, &i.IsDefault, &i.OrderPosition, &i.SessionCount, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getWorkspacesByUserID = `-- name: GetWorkspacesByUserID :many SELECT id, uuid, user_id, name, description, color, icon, created_at, updated_at, is_default, order_position FROM chat_workspace WHERE user_id = $1 ORDER BY order_position ASC, created_at ASC ` func (q *Queries) GetWorkspacesByUserID(ctx context.Context, userID int32) ([]ChatWorkspace, error) { rows, err := q.db.QueryContext(ctx, getWorkspacesByUserID, userID) if err != nil { return nil, err } defer rows.Close() var items []ChatWorkspace for rows.Next() { var i ChatWorkspace if err := rows.Scan( &i.ID, &i.Uuid, &i.UserID, &i.Name, &i.Description, &i.Color, &i.Icon, &i.CreatedAt, &i.UpdatedAt, &i.IsDefault, &i.OrderPosition, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const hasWorkspacePermission = `-- name: HasWorkspacePermission :one SELECT COUNT(*) > 0 as has_permission FROM chat_workspace w WHERE w.uuid = $1 AND ( w.user_id = $2 OR EXISTS ( SELECT 1 FROM auth_user request_user WHERE request_user.id = $2 AND request_user.is_superuser = true ) ) ` type HasWorkspacePermissionParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` } func (q *Queries) HasWorkspacePermission(ctx context.Context, arg HasWorkspacePermissionParams) (bool, error) { row := q.db.QueryRowContext(ctx, hasWorkspacePermission, arg.Uuid, arg.UserID) var has_permission bool err := row.Scan(&has_permission) return has_permission, err } const setDefaultWorkspace = `-- name: SetDefaultWorkspace :one UPDATE chat_workspace SET is_default = $2, updated_at = now() WHERE uuid = $1 RETURNING id, uuid, user_id, name, description, color, icon, created_at, updated_at, is_default, order_position ` type SetDefaultWorkspaceParams struct { Uuid string `json:"uuid"` IsDefault bool `json:"isDefault"` } func (q *Queries) SetDefaultWorkspace(ctx context.Context, arg SetDefaultWorkspaceParams) (ChatWorkspace, error) { row := q.db.QueryRowContext(ctx, setDefaultWorkspace, arg.Uuid, arg.IsDefault) var i ChatWorkspace err := row.Scan( &i.ID, &i.Uuid, &i.UserID, &i.Name, &i.Description, &i.Color, &i.Icon, &i.CreatedAt, &i.UpdatedAt, &i.IsDefault, &i.OrderPosition, ) return i, err } const updateWorkspace = `-- name: UpdateWorkspace :one UPDATE chat_workspace SET name = $2, description = $3, color = $4, icon = $5, updated_at = now() WHERE uuid = $1 RETURNING id, uuid, user_id, name, description, color, icon, created_at, updated_at, is_default, order_position ` type UpdateWorkspaceParams struct { Uuid string `json:"uuid"` Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` } func (q *Queries) UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (ChatWorkspace, error) { row := q.db.QueryRowContext(ctx, updateWorkspace, arg.Uuid, arg.Name, arg.Description, arg.Color, arg.Icon, ) var i ChatWorkspace err := row.Scan( &i.ID, &i.Uuid, &i.UserID, &i.Name, &i.Description, &i.Color, &i.Icon, &i.CreatedAt, &i.UpdatedAt, &i.IsDefault, &i.OrderPosition, ) return i, err } const updateWorkspaceOrder = `-- name: UpdateWorkspaceOrder :one UPDATE chat_workspace SET order_position = $2, updated_at = now() WHERE uuid = $1 RETURNING id, uuid, user_id, name, description, color, icon, created_at, updated_at, is_default, order_position ` type UpdateWorkspaceOrderParams struct { Uuid string `json:"uuid"` OrderPosition int32 `json:"orderPosition"` } func (q *Queries) UpdateWorkspaceOrder(ctx context.Context, arg UpdateWorkspaceOrderParams) (ChatWorkspace, error) { row := q.db.QueryRowContext(ctx, updateWorkspaceOrder, arg.Uuid, arg.OrderPosition) var i ChatWorkspace err := row.Scan( &i.ID, &i.Uuid, &i.UserID, &i.Name, &i.Description, &i.Color, &i.Icon, &i.CreatedAt, &i.UpdatedAt, &i.IsDefault, &i.OrderPosition, ) return i, err } ================================================ FILE: api/sqlc_queries/db.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 package sqlc_queries import ( "context" "database/sql" ) type DBTX interface { ExecContext(context.Context, string, ...interface{}) (sql.Result, error) PrepareContext(context.Context, string) (*sql.Stmt, error) QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) QueryRowContext(context.Context, string, ...interface{}) *sql.Row } func New(db DBTX) *Queries { return &Queries{db: db} } type Queries struct { db DBTX } func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ db: tx, } } ================================================ FILE: api/sqlc_queries/jwt_secrets.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: jwt_secrets.sql package sqlc_queries import ( "context" ) const createJwtSecret = `-- name: CreateJwtSecret :one INSERT INTO jwt_secrets (name, secret, audience) VALUES ($1, $2, $3) RETURNING id, name, secret, audience, lifetime ` type CreateJwtSecretParams struct { Name string `json:"name"` Secret string `json:"secret"` Audience string `json:"audience"` } func (q *Queries) CreateJwtSecret(ctx context.Context, arg CreateJwtSecretParams) (JwtSecret, error) { row := q.db.QueryRowContext(ctx, createJwtSecret, arg.Name, arg.Secret, arg.Audience) var i JwtSecret err := row.Scan( &i.ID, &i.Name, &i.Secret, &i.Audience, &i.Lifetime, ) return i, err } const deleteAllJwtSecrets = `-- name: DeleteAllJwtSecrets :execrows DELETE FROM jwt_secrets ` func (q *Queries) DeleteAllJwtSecrets(ctx context.Context) (int64, error) { result, err := q.db.ExecContext(ctx, deleteAllJwtSecrets) if err != nil { return 0, err } return result.RowsAffected() } const getJwtSecret = `-- name: GetJwtSecret :one SELECT id, name, secret, audience, lifetime FROM jwt_secrets WHERE name = $1 ` func (q *Queries) GetJwtSecret(ctx context.Context, name string) (JwtSecret, error) { row := q.db.QueryRowContext(ctx, getJwtSecret, name) var i JwtSecret err := row.Scan( &i.ID, &i.Name, &i.Secret, &i.Audience, &i.Lifetime, ) return i, err } ================================================ FILE: api/sqlc_queries/models.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 package sqlc_queries import ( "database/sql" "encoding/json" "time" ) type AuthUser struct { ID int32 `json:"id"` Password string `json:"password"` LastLogin time.Time `json:"lastLogin"` IsSuperuser bool `json:"isSuperuser"` Username string `json:"username"` FirstName string `json:"firstName"` LastName string `json:"lastName"` Email string `json:"email"` IsStaff bool `json:"isStaff"` IsActive bool `json:"isActive"` DateJoined time.Time `json:"dateJoined"` } type AuthUserManagement struct { ID int32 `json:"id"` UserID int32 `json:"userId"` RateLimit int32 `json:"rateLimit"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type BotAnswerHistory struct { ID int32 `json:"id"` BotUuid string `json:"botUuid"` UserID int32 `json:"userId"` Prompt string `json:"prompt"` Answer string `json:"answer"` Model string `json:"model"` TokensUsed int32 `json:"tokensUsed"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type ChatComment struct { ID int32 `json:"id"` Uuid string `json:"uuid"` ChatSessionUuid string `json:"chatSessionUuid"` ChatMessageUuid string `json:"chatMessageUuid"` Content string `json:"content"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` CreatedBy int32 `json:"createdBy"` UpdatedBy int32 `json:"updatedBy"` } type ChatFile struct { ID int32 `json:"id"` Name string `json:"name"` Data []byte `json:"data"` CreatedAt time.Time `json:"createdAt"` UserID int32 `json:"userId"` ChatSessionUuid string `json:"chatSessionUuid"` MimeType string `json:"mimeType"` } type ChatLog struct { ID int32 `json:"id"` Session json.RawMessage `json:"session"` Question json.RawMessage `json:"question"` Answer json.RawMessage `json:"answer"` CreatedAt time.Time `json:"createdAt"` } type ChatMessage struct { ID int32 `json:"id"` Uuid string `json:"uuid"` ChatSessionUuid string `json:"chatSessionUuid"` Role string `json:"role"` Content string `json:"content"` ReasoningContent string `json:"reasoningContent"` Model string `json:"model"` LlmSummary string `json:"llmSummary"` Score float64 `json:"score"` UserID int32 `json:"userId"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` CreatedBy int32 `json:"createdBy"` UpdatedBy int32 `json:"updatedBy"` IsDeleted bool `json:"isDeleted"` IsPin bool `json:"isPin"` TokenCount int32 `json:"tokenCount"` Raw json.RawMessage `json:"raw"` Artifacts json.RawMessage `json:"artifacts"` SuggestedQuestions json.RawMessage `json:"suggestedQuestions"` } type ChatModel struct { ID int32 `json:"id"` Name string `json:"name"` Label string `json:"label"` IsDefault bool `json:"isDefault"` Url string `json:"url"` ApiAuthHeader string `json:"apiAuthHeader"` ApiAuthKey string `json:"apiAuthKey"` UserID int32 `json:"userId"` EnablePerModeRatelimit bool `json:"enablePerModeRatelimit"` MaxToken int32 `json:"maxToken"` DefaultToken int32 `json:"defaultToken"` OrderNumber int32 `json:"orderNumber"` HttpTimeOut int32 `json:"httpTimeOut"` IsEnable bool `json:"isEnable"` ApiType string `json:"apiType"` } type ChatPrompt struct { ID int32 `json:"id"` Uuid string `json:"uuid"` ChatSessionUuid string `json:"chatSessionUuid"` Role string `json:"role"` Content string `json:"content"` Score float64 `json:"score"` UserID int32 `json:"userId"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` CreatedBy int32 `json:"createdBy"` UpdatedBy int32 `json:"updatedBy"` IsDeleted bool `json:"isDeleted"` TokenCount int32 `json:"tokenCount"` } type ChatSession struct { ID int32 `json:"id"` UserID int32 `json:"userId"` Uuid string `json:"uuid"` Topic string `json:"topic"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` Active bool `json:"active"` Model string `json:"model"` MaxLength int32 `json:"maxLength"` Temperature float64 `json:"temperature"` TopP float64 `json:"topP"` MaxTokens int32 `json:"maxTokens"` N int32 `json:"n"` SummarizeMode bool `json:"summarizeMode"` WorkspaceID sql.NullInt32 `json:"workspaceId"` ArtifactEnabled bool `json:"artifactEnabled"` Debug bool `json:"debug"` ExploreMode bool `json:"exploreMode"` } type ChatSnapshot struct { ID int32 `json:"id"` Typ string `json:"typ"` Uuid string `json:"uuid"` UserID int32 `json:"userId"` Title string `json:"title"` Summary string `json:"summary"` Model string `json:"model"` Tags json.RawMessage `json:"tags"` Session json.RawMessage `json:"session"` Conversation json.RawMessage `json:"conversation"` CreatedAt time.Time `json:"createdAt"` Text string `json:"text"` SearchVector interface{} `json:"searchVector"` } type ChatWorkspace struct { ID int32 `json:"id"` Uuid string `json:"uuid"` UserID int32 `json:"userId"` Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` Icon string `json:"icon"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` IsDefault bool `json:"isDefault"` OrderPosition int32 `json:"orderPosition"` } type JwtSecret struct { ID int32 `json:"id"` Name string `json:"name"` Secret string `json:"secret"` Audience string `json:"audience"` Lifetime int16 `json:"lifetime"` } type UserActiveChatSession struct { ID int32 `json:"id"` UserID int32 `json:"userId"` ChatSessionUuid string `json:"chatSessionUuid"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` WorkspaceID sql.NullInt32 `json:"workspaceId"` } type UserChatModelPrivilege struct { ID int32 `json:"id"` UserID int32 `json:"userId"` ChatModelID int32 `json:"chatModelId"` RateLimit int32 `json:"rateLimit"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` CreatedBy int32 `json:"createdBy"` UpdatedBy int32 `json:"updatedBy"` } ================================================ FILE: api/sqlc_queries/user_active_chat_session.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: user_active_chat_session.sql package sqlc_queries import ( "context" "database/sql" ) const deleteUserActiveSession = `-- name: DeleteUserActiveSession :exec DELETE FROM user_active_chat_session WHERE user_id = $1 AND ( (workspace_id IS NULL AND $2::int IS NULL) OR (workspace_id = $2) ) ` type DeleteUserActiveSessionParams struct { UserID int32 `json:"userId"` Column2 int32 `json:"column2"` } func (q *Queries) DeleteUserActiveSession(ctx context.Context, arg DeleteUserActiveSessionParams) error { _, err := q.db.ExecContext(ctx, deleteUserActiveSession, arg.UserID, arg.Column2) return err } const deleteUserActiveSessionBySession = `-- name: DeleteUserActiveSessionBySession :exec DELETE FROM user_active_chat_session WHERE user_id = $1 AND chat_session_uuid = $2 ` type DeleteUserActiveSessionBySessionParams struct { UserID int32 `json:"userId"` ChatSessionUuid string `json:"chatSessionUuid"` } func (q *Queries) DeleteUserActiveSessionBySession(ctx context.Context, arg DeleteUserActiveSessionBySessionParams) error { _, err := q.db.ExecContext(ctx, deleteUserActiveSessionBySession, arg.UserID, arg.ChatSessionUuid) return err } const getAllUserActiveSessions = `-- name: GetAllUserActiveSessions :many SELECT id, user_id, chat_session_uuid, created_at, updated_at, workspace_id FROM user_active_chat_session WHERE user_id = $1 ORDER BY workspace_id NULLS FIRST, updated_at DESC ` func (q *Queries) GetAllUserActiveSessions(ctx context.Context, userID int32) ([]UserActiveChatSession, error) { rows, err := q.db.QueryContext(ctx, getAllUserActiveSessions, userID) if err != nil { return nil, err } defer rows.Close() var items []UserActiveChatSession for rows.Next() { var i UserActiveChatSession if err := rows.Scan( &i.ID, &i.UserID, &i.ChatSessionUuid, &i.CreatedAt, &i.UpdatedAt, &i.WorkspaceID, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getUserActiveSession = `-- name: GetUserActiveSession :one SELECT id, user_id, chat_session_uuid, created_at, updated_at, workspace_id FROM user_active_chat_session WHERE user_id = $1 AND ( (workspace_id IS NULL AND $2::int IS NULL) OR (workspace_id = $2) ) ` type GetUserActiveSessionParams struct { UserID int32 `json:"userId"` Column2 int32 `json:"column2"` } func (q *Queries) GetUserActiveSession(ctx context.Context, arg GetUserActiveSessionParams) (UserActiveChatSession, error) { row := q.db.QueryRowContext(ctx, getUserActiveSession, arg.UserID, arg.Column2) var i UserActiveChatSession err := row.Scan( &i.ID, &i.UserID, &i.ChatSessionUuid, &i.CreatedAt, &i.UpdatedAt, &i.WorkspaceID, ) return i, err } const upsertUserActiveSession = `-- name: UpsertUserActiveSession :one INSERT INTO user_active_chat_session (user_id, workspace_id, chat_session_uuid) VALUES ($1, $2, $3) ON CONFLICT (user_id, COALESCE(workspace_id, -1)) DO UPDATE SET chat_session_uuid = EXCLUDED.chat_session_uuid, updated_at = now() RETURNING id, user_id, chat_session_uuid, created_at, updated_at, workspace_id ` type UpsertUserActiveSessionParams struct { UserID int32 `json:"userId"` WorkspaceID sql.NullInt32 `json:"workspaceId"` ChatSessionUuid string `json:"chatSessionUuid"` } // Simplified unified queries for active sessions func (q *Queries) UpsertUserActiveSession(ctx context.Context, arg UpsertUserActiveSessionParams) (UserActiveChatSession, error) { row := q.db.QueryRowContext(ctx, upsertUserActiveSession, arg.UserID, arg.WorkspaceID, arg.ChatSessionUuid) var i UserActiveChatSession err := row.Scan( &i.ID, &i.UserID, &i.ChatSessionUuid, &i.CreatedAt, &i.UpdatedAt, &i.WorkspaceID, ) return i, err } ================================================ FILE: api/sqlc_queries/user_chat_model_privilege.sql.go ================================================ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 // source: user_chat_model_privilege.sql package sqlc_queries import ( "context" ) const createUserChatModelPrivilege = `-- name: CreateUserChatModelPrivilege :one INSERT INTO user_chat_model_privilege (user_id, chat_model_id, rate_limit, created_by, updated_by) VALUES ($1, $2, $3, $4, $5) RETURNING id, user_id, chat_model_id, rate_limit, created_at, updated_at, created_by, updated_by ` type CreateUserChatModelPrivilegeParams struct { UserID int32 `json:"userId"` ChatModelID int32 `json:"chatModelId"` RateLimit int32 `json:"rateLimit"` CreatedBy int32 `json:"createdBy"` UpdatedBy int32 `json:"updatedBy"` } func (q *Queries) CreateUserChatModelPrivilege(ctx context.Context, arg CreateUserChatModelPrivilegeParams) (UserChatModelPrivilege, error) { row := q.db.QueryRowContext(ctx, createUserChatModelPrivilege, arg.UserID, arg.ChatModelID, arg.RateLimit, arg.CreatedBy, arg.UpdatedBy, ) var i UserChatModelPrivilege err := row.Scan( &i.ID, &i.UserID, &i.ChatModelID, &i.RateLimit, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, ) return i, err } const deleteUserChatModelPrivilege = `-- name: DeleteUserChatModelPrivilege :exec DELETE FROM user_chat_model_privilege WHERE id = $1 ` func (q *Queries) DeleteUserChatModelPrivilege(ctx context.Context, id int32) error { _, err := q.db.ExecContext(ctx, deleteUserChatModelPrivilege, id) return err } const listUserChatModelPrivileges = `-- name: ListUserChatModelPrivileges :many SELECT id, user_id, chat_model_id, rate_limit, created_at, updated_at, created_by, updated_by FROM user_chat_model_privilege ORDER BY id ` func (q *Queries) ListUserChatModelPrivileges(ctx context.Context) ([]UserChatModelPrivilege, error) { rows, err := q.db.QueryContext(ctx, listUserChatModelPrivileges) if err != nil { return nil, err } defer rows.Close() var items []UserChatModelPrivilege for rows.Next() { var i UserChatModelPrivilege if err := rows.Scan( &i.ID, &i.UserID, &i.ChatModelID, &i.RateLimit, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listUserChatModelPrivilegesByUserID = `-- name: ListUserChatModelPrivilegesByUserID :many SELECT id, user_id, chat_model_id, rate_limit, created_at, updated_at, created_by, updated_by FROM user_chat_model_privilege WHERE user_id = $1 ORDER BY id ` // TODO add ratelimit // LIMIT 1000 func (q *Queries) ListUserChatModelPrivilegesByUserID(ctx context.Context, userID int32) ([]UserChatModelPrivilege, error) { rows, err := q.db.QueryContext(ctx, listUserChatModelPrivilegesByUserID, userID) if err != nil { return nil, err } defer rows.Close() var items []UserChatModelPrivilege for rows.Next() { var i UserChatModelPrivilege if err := rows.Scan( &i.ID, &i.UserID, &i.ChatModelID, &i.RateLimit, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listUserChatModelPrivilegesRateLimit = `-- name: ListUserChatModelPrivilegesRateLimit :many SELECT ucmp.id, au.email as user_email, CONCAT_WS('',au.last_name, au.first_name) as full_name, cm.name chat_model_name, ucmp.rate_limit FROM user_chat_model_privilege ucmp INNER JOIN chat_model cm ON cm.id = ucmp.chat_model_id INNER JOIN auth_user au ON au.id = ucmp.user_id ORDER by au.last_login DESC ` type ListUserChatModelPrivilegesRateLimitRow struct { ID int32 `json:"id"` UserEmail string `json:"userEmail"` FullName string `json:"fullName"` ChatModelName string `json:"chatModelName"` RateLimit int32 `json:"rateLimit"` } func (q *Queries) ListUserChatModelPrivilegesRateLimit(ctx context.Context) ([]ListUserChatModelPrivilegesRateLimitRow, error) { rows, err := q.db.QueryContext(ctx, listUserChatModelPrivilegesRateLimit) if err != nil { return nil, err } defer rows.Close() var items []ListUserChatModelPrivilegesRateLimitRow for rows.Next() { var i ListUserChatModelPrivilegesRateLimitRow if err := rows.Scan( &i.ID, &i.UserEmail, &i.FullName, &i.ChatModelName, &i.RateLimit, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const rateLimiteByUserAndSessionUUID = `-- name: RateLimiteByUserAndSessionUUID :one SELECT ucmp.rate_limit, cm.name AS chat_model_name FROM user_chat_model_privilege ucmp JOIN chat_session cs ON cs.user_id = ucmp.user_id JOIN chat_model cm ON (cm.id = ucmp.chat_model_id AND cs.model = cm.name and cm.enable_per_mode_ratelimit = true) WHERE cs.uuid = $1 AND ucmp.user_id = $2 ` type RateLimiteByUserAndSessionUUIDParams struct { Uuid string `json:"uuid"` UserID int32 `json:"userId"` } type RateLimiteByUserAndSessionUUIDRow struct { RateLimit int32 `json:"rateLimit"` ChatModelName string `json:"chatModelName"` } func (q *Queries) RateLimiteByUserAndSessionUUID(ctx context.Context, arg RateLimiteByUserAndSessionUUIDParams) (RateLimiteByUserAndSessionUUIDRow, error) { row := q.db.QueryRowContext(ctx, rateLimiteByUserAndSessionUUID, arg.Uuid, arg.UserID) var i RateLimiteByUserAndSessionUUIDRow err := row.Scan(&i.RateLimit, &i.ChatModelName) return i, err } const updateUserChatModelPrivilege = `-- name: UpdateUserChatModelPrivilege :one UPDATE user_chat_model_privilege SET rate_limit = $2, updated_at = now(), updated_by = $3 WHERE id = $1 RETURNING id, user_id, chat_model_id, rate_limit, created_at, updated_at, created_by, updated_by ` type UpdateUserChatModelPrivilegeParams struct { ID int32 `json:"id"` RateLimit int32 `json:"rateLimit"` UpdatedBy int32 `json:"updatedBy"` } func (q *Queries) UpdateUserChatModelPrivilege(ctx context.Context, arg UpdateUserChatModelPrivilegeParams) (UserChatModelPrivilege, error) { row := q.db.QueryRowContext(ctx, updateUserChatModelPrivilege, arg.ID, arg.RateLimit, arg.UpdatedBy) var i UserChatModelPrivilege err := row.Scan( &i.ID, &i.UserID, &i.ChatModelID, &i.RateLimit, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, ) return i, err } const userChatModelPrivilegeByID = `-- name: UserChatModelPrivilegeByID :one SELECT id, user_id, chat_model_id, rate_limit, created_at, updated_at, created_by, updated_by FROM user_chat_model_privilege WHERE id = $1 ` func (q *Queries) UserChatModelPrivilegeByID(ctx context.Context, id int32) (UserChatModelPrivilege, error) { row := q.db.QueryRowContext(ctx, userChatModelPrivilegeByID, id) var i UserChatModelPrivilege err := row.Scan( &i.ID, &i.UserID, &i.ChatModelID, &i.RateLimit, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, ) return i, err } const userChatModelPrivilegeByUserAndModelID = `-- name: UserChatModelPrivilegeByUserAndModelID :one SELECT id, user_id, chat_model_id, rate_limit, created_at, updated_at, created_by, updated_by FROM user_chat_model_privilege WHERE user_id = $1 AND chat_model_id = $2 ` type UserChatModelPrivilegeByUserAndModelIDParams struct { UserID int32 `json:"userId"` ChatModelID int32 `json:"chatModelId"` } func (q *Queries) UserChatModelPrivilegeByUserAndModelID(ctx context.Context, arg UserChatModelPrivilegeByUserAndModelIDParams) (UserChatModelPrivilege, error) { row := q.db.QueryRowContext(ctx, userChatModelPrivilegeByUserAndModelID, arg.UserID, arg.ChatModelID) var i UserChatModelPrivilege err := row.Scan( &i.ID, &i.UserID, &i.ChatModelID, &i.RateLimit, &i.CreatedAt, &i.UpdatedAt, &i.CreatedBy, &i.UpdatedBy, ) return i, err } ================================================ FILE: api/sqlc_queries/zz_custom_method.go ================================================ package sqlc_queries import ( "context" "encoding/json" "github.com/samber/lo" "github.com/sashabaranov/go-openai" ) func (user *AuthUser) Role() string { role := "user" if user.IsSuperuser { role = "admin" } return role } func (m *ChatMessage) Authenticate(q Queries, userID int32) (bool, error) { messageID := m.ID ctx := context.Background() v, e := q.HasChatMessagePermission(ctx, HasChatMessagePermissionParams{messageID, userID}) return v, e } func (s *ChatSession) Authenticate(q Queries, userID int32) (bool, error) { sessionID := s.ID ctx := context.Background() v, e := q.HasChatSessionPermission(ctx, HasChatSessionPermissionParams{sessionID, userID}) return v, e } func (p *ChatPrompt) Authenticate(q Queries, userID int32) (bool, error) { sessionID := p.ID ctx := context.Background() v, e := q.HasChatPromptPermission(ctx, HasChatPromptPermissionParams{sessionID, userID}) return v, e } // Create a RawMessage from ChatSession func (cs *ChatSession) ToRawMessage() *json.RawMessage { // Marshal ChatSession struct to json.RawMessage chatSessionJSON, err := json.Marshal(cs) if err != nil { return nil } var rawMessage json.RawMessage = chatSessionJSON return &rawMessage } type MessageWithRoleAndContent interface { GetRole() string GetContent() string } func (m ChatMessage) GetRole() string { return m.Role } func (m ChatMessage) GetContent() string { return m.Content } func (m ChatPrompt) GetRole() string { return m.Role } func (m ChatPrompt) GetContent() string { return m.Content } func SqlChatsToOpenAIMesages(messages []MessageWithRoleAndContent) []openai.ChatCompletionMessage { open_ai_msgs := lo.Map(messages, func(m MessageWithRoleAndContent, _ int) openai.ChatCompletionMessage { return openai.ChatCompletionMessage{Role: m.GetRole(), Content: m.GetContent()} }) return open_ai_msgs } func SqlChatsToOpenAIMessagesGenerics[T MessageWithRoleAndContent](messages []T) []openai.ChatCompletionMessage { open_ai_msgs := lo.Map(messages, func(m T, _ int) openai.ChatCompletionMessage { return openai.ChatCompletionMessage{Role: m.GetRole(), Content: m.GetContent()} }) return open_ai_msgs } // TODO: How to write generics function without create new interface? // // SumIntsOrFloats sums the values of map m. It supports both floats and integers // // as map values. // func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { // var s V // for _, v := range m { // s += v // } // return s // } // func ConvertToMessages[T ChatPrompt | ChatMessage](input []T) []openai.ChatCompletionMessage { // // Define an empty slice to hold the converted messages // output := make([]openai.ChatCompletionMessage, 0) // // Loop over the input slice and convert each element to a Message // for _, obj := range input { // output = append(output, openai.ChatCompletionMessage{ // Role: obj.Role, // Content: obj.Content, // }) // } // return output // } // """ // type ChatMessage struct { // ID int32 // Uuid string // ChatSessionUuid string // Role string // Content string // } // type ChatPrompt struct { // ID int32 // Uuid string // ChatSessionUuid string // Role string // Content string // Score float64 // } // type Message struct { // Role string // Content string // } // """ // please write a generic method that convert a list of ChatPrompt or ChatMessage to Message in golang ================================================ FILE: api/sqlc_queries/zz_custom_query.go ================================================ package sqlc_queries import ( "context" "encoding/json" "time" "github.com/rotisserie/eris" "github.com/samber/lo" ) type SimpleChatMessage struct { Uuid string `json:"uuid"` DateTime string `json:"dateTime"` Text string `json:"text"` Model string `json:"model"` Inversion bool `json:"inversion"` Error bool `json:"error"` Loading bool `json:"loading"` IsPin bool `json:"isPin"` IsPrompt bool `json:"isPrompt"` Artifacts []Artifact `json:"artifacts,omitempty"` SuggestedQuestions []string `json:"suggestedQuestions,omitempty"` } type Artifact struct { UUID string `json:"uuid"` Type string `json:"type"` Title string `json:"title"` Content string `json:"content"` Language string `json:"language,omitempty"` } func (q *Queries) GetChatHistoryBySessionUUID(ctx context.Context, uuid string, pageNum, pageSize int32) ([]SimpleChatMessage, error) { chat_prompts, err := q.GetChatPromptsBySessionUUID(ctx, uuid) if err != nil { return nil, eris.Wrap(err, "fail to get prompt: ") } simple_prompts := lo.Map(chat_prompts, func(prompt ChatPrompt, idx int) SimpleChatMessage { return SimpleChatMessage{ Uuid: prompt.Uuid, DateTime: prompt.UpdatedAt.Format(time.RFC3339), Text: prompt.Content, Inversion: idx%2 == 0, Error: false, Loading: false, IsPin: false, IsPrompt: true, } }) messages, err := q.GetChatMessagesBySessionUUID(ctx, GetChatMessagesBySessionUUIDParams{ Uuid: uuid, Offset: pageNum - 1, Limit: pageSize, }) if err != nil { return nil, eris.Wrap(err, "fail to get message: ") } simple_msgs := lo.Map(messages, func(message ChatMessage, _ int) SimpleChatMessage { text := message.Content // prepend reason content if len(message.ReasoningContent) > 0 { text = message.ReasoningContent + message.Content } // Extract artifacts from database var artifacts []Artifact if message.Artifacts != nil { err := json.Unmarshal(message.Artifacts, &artifacts) if err != nil { // Log error but don't fail the request artifacts = []Artifact{} } } // Extract suggested questions from database var suggestedQuestions []string if message.SuggestedQuestions != nil { err := json.Unmarshal(message.SuggestedQuestions, &suggestedQuestions) if err != nil { // Log error but don't fail the request suggestedQuestions = []string{} } } return SimpleChatMessage{ Uuid: message.Uuid, DateTime: message.UpdatedAt.Format(time.RFC3339), Text: text, Model: message.Model, Inversion: message.Role == "user", Error: false, Loading: false, IsPin: message.IsPin, Artifacts: artifacts, SuggestedQuestions: suggestedQuestions, } }) msgs := append(simple_prompts, simple_msgs...) return msgs, nil } ================================================ FILE: api/static/awesome-chatgpt-prompts-en.json ================================================ [ { "key": "Act As a UX/UI Designer", "value": "I want you to act as a UX/UI developer. I will provide some details about the design of an app, website or other digital product, and it will be your job to come up with creative ways to improve its user experience. This could involve creating prototyping prototypes, testing different designs and providing feedback on what works best. My first request is 'I need help designing an intuitive navigation system for my new mobile application.'" }, { "key": "Act as a Web Design Consultant", "value": "I want you to act as a web design consultant. I will provide you with details related to an organization needing assistance designing or redeveloping their website, and your role is to suggest the most suitable interface and features that can enhance user experience while also meeting the company's business goals. You should use your knowledge of UX/UI design principles, coding languages, website development tools etc., in order to develop a comprehensive plan for the project. " }, { "key": "Act as a Prompt generator", "value": "I want you to act as a prompt generator. Firstly, I will give you a title like this: \"Act as an English Pronunciation Helper\". Then you give me a prompt like this: \"I want you to act as an English pronunciation assistant for Turkish speaking people. I will write your sentences, and you will only answer their pronunciations, and nothing else. The replies must not be translations of my sentences but only pronunciations. Pronunciations should use Turkish Latin letters for phonetics. Do not write explanations on replies. My first sentence is \"how the weather is in Istanbul?\".\" (You should adapt the sample prompt according to the title I gave. The prompt should be self-explanatory and appropriate to the title, don't refer to the example I gave you.). My first title is \"Act as a Code Review Helper\" (Give me prompt only)" }, { "key": "Act as Tester", "value": "I want you to act as a software quality assurance tester for a new software application. Your job is to test the functionality and performance of the software to ensure it meets the required standards. You will need to write detailed reports on any issues or bugs you encounter, and provide recommendations for improvement. Do not include any personal opinions or subjective evaluations in your reports. Your first task is to test the login functionality of the software." }, { "key": "Act as an IT Architect", "value": "I want you to act as an IT Architect. I will provide some details about the functionality of an application or other digital product, and it will be your job to come up with ways to integrate it into the IT landscape. This could involve analyzing business requirements, performing a gap analysis and mapping the functionality of the new system to the existing IT landscape. Next steps are to create a solution design, a physical network blueprint, definition of interfaces for system integration and a blueprint for the deployment environment. My first request is \"I need help to integrate a CMS system.\"" }, { "key": "Act as a Histrian", "value": "I want you to act as a historian. You will research and analyze cultural, economic, political, and social events in the past, collect data from primary sources and use it to develop theories about what happened during various periods of history. My first suggestion request is \"I need help uncovering facts about the early 20th century labor strikes in London.\"" }, { "key": "Act as a Tech Writer", "value": "Act as a tech writer. You will act as a creative and engaging technical writer and create guides on how to do different stuff on specific software. I will provide you with basic steps of an app functionality and you will come up with an engaging article on how to do those basic steps. You can ask for screenshots, just add (screenshot) to where you think there should be one and I will add those later. These are the first basic steps of the app functionality: \"1.Click on the download button depending on your platform 2.Install the file. 3.Double click to open the app\"" }, { "key": "Act as a Machine Learning Engineer", "value": "I want you to act as a machine learning engineer. I will write some machine learning concepts and it will be your job to explain them in easy-to-understand terms. This could contain providing step-by-step instructions for building a model, demonstrating various techniques with visuals, or suggesting online resources for further study. My first suggestion request is \"I have a dataset without labels. Which machine learning algorithm should I use?\"" }, { "key": "Act as an IT Expert", "value": "I want you to act as an IT Expert. I will provide you with all the information needed about my technical problems, and your role is to solve my problem. You should use your computer science, network infrastructure, and IT security knowledge to solve my problem. Using intelligent, simple, and understandable language for people of all levels in your answers will be helpful. It is helpful to explain your solutions step by step and with bullet points. Try to avoid too many technical details, but use them when necessary. I want you to reply with the solution, not write any explanations. My first problem is \"my laptop gets an error with a blue screen.\"" }, { "key": "Act as a proofreader", "value": "I want you act as a proofreader. I will provide you texts and I would like you to review them for any spelling, grammar, or punctuation errors. Once you have finished reviewing the text, provide me with any necessary corrections or suggestions for improve the text." } ] ================================================ FILE: api/static/awesome-chatgpt-prompts-zh.json ================================================ [ { "key": "充当 Linux 终端", "value": "我想让你充当 Linux 终端。我将输入命令,您将回复终端应显示的内容。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会把文字放在中括号内[就像这样]。我的第一个命令是 pwd" }, { "key": "充当英语翻译和改进者", "value": "我希望你能担任英语翻译、拼写校对和修辞改进的角色。我会用任何语言和你交流,你会识别语言,将其翻译并用更为优美和精炼的英语回答我。请将我简单的词汇和句子替换成更为优美和高雅的表达方式,确保意思不变,但使其更具文学性。请仅回答更正和改进的部分,不要写解释。我的第一句话是“how are you ?”,请翻译它。" }, { "key": "充当英翻中", "value": "下面我让你来充当翻译家,你的目标是把任何语言翻译成中文,请翻译时不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面这句话:“how are you ?”" }, { "key": "充当英文词典(附中文解释)", "value": "我想让你充当英文词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈,第一个单词是“Hello\"" }, { "key": "充当前端智能思路助手", "value": "我想让你充当前端开发专家。我将提供一些关于Js、Node等前端代码问题的具体信息,而你的工作就是想出为我解决问题的策略。这可能包括建议代码、代码逻辑思路策略。我的第一个请求是“我需要能够动态监听某个元素节点距离当前电脑设备屏幕的左上角的X和Y轴,通过拖拽移动位置浏览器窗口和改变大小浏览器窗口。”" }, { "key": "担任面试官", "value": "我想让你担任Android开发工程师面试官。我将成为候选人,您将向我询问Android开发工程师职位的面试问题。我希望你只作为面试官回答。不要一次写出所有的问题。我希望你只对我进行采访。问我问题,等待我的回答。不要写解释。像面试官一样一个一个问我,等我回答。我的第一句话是“面试官你好”" }, { "key": "充当 JavaScript 控制台", "value": "我希望你充当 javascript 控制台。我将键入命令,您将回复 javascript 控制台应显示的内容。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非我指示您这样做。我的第一个命令是 console.log(\"Hello World\");" }, { "key": "充当 Excel 工作表", "value": "我希望你充当基于文本的 excel。您只会回复我基于文本的 10 行 Excel 工作表,其中行号和单元格字母作为列(A 到 L)。第一列标题应为空以引用行号。我会告诉你在单元格中写入什么,你只会以文本形式回复 excel 表格的结果,而不是其他任何内容。不要写解释。我会写你的公式,你会执行公式,你只会回复 excel 表的结果作为文本。首先,回复我空表。" }, { "key": "充当英语发音帮手", "value": "我想让你为说汉语的人充当英语发音助手。我会给你写句子,你只会回答他们的发音,没有别的。回复不能是我的句子的翻译,而只能是发音。发音应使用汉语谐音进行注音。不要在回复上写解释。我的第一句话是“上海的天气怎么样?”" }, { "key": "充当旅游指南", "value": "我想让你做一个旅游指南。我会把我的位置写给你,你会推荐一个靠近我的位置的地方。在某些情况下,我还会告诉您我将访问的地方类型。您还会向我推荐靠近我的第一个位置的类似类型的地方。我的第一个建议请求是“我在上海,我只想参观博物馆。”" }, { "key": "充当抄袭检查员", "value": "我想让你充当剽窃检查员。我会给你写句子,你只会用给定句子的语言在抄袭检查中未被发现的情况下回复,别无其他。不要在回复上写解释。我的第一句话是“为了让计算机像人类一样行动,语音识别系统必须能够处理非语言信息,例如说话者的情绪状态。”" }, { "key": "充当“电影/书籍/任何东西”中的“角色”", "value": "我希望你表现得像{series} 中的{Character}。我希望你像{Character}一样回应和回答。不要写任何解释。只回答像{character}。你必须知道{character}的所有知识。我的第一句话是“你好”" }, { "key": "作为广告商", "value": "我想让你充当广告商。您将创建一个活动来推广您选择的产品或服务。您将选择目标受众,制定关键信息和口号,选择宣传媒体渠道,并决定实现目标所需的任何其他活动。我的第一个建议请求是“我需要帮助针对 18-30 岁的年轻人制作一种新型能量饮料的广告活动。”" }, { "key": "充当讲故事的人", "value": "我想让你扮演讲故事的角色。您将想出引人入胜、富有想象力和吸引观众的有趣故事。它可以是童话故事、教育故事或任何其他类型的故事,有可能吸引人们的注意力和想象力。根据目标受众,您可以为讲故事环节选择特定的主题或主题,例如,如果是儿童,则可以谈论动物;如果是成年人,那么基于历史的故事可能会更好地吸引他们等等。我的第一个要求是“我需要一个关于毅力的有趣故事。”" }, { "key": "担任足球解说员", "value": "我想让你担任足球评论员。我会给你描述正在进行的足球比赛,你会评论比赛,分析到目前为止发生的事情,并预测比赛可能会如何结束。您应该了解足球术语、战术、每场比赛涉及的球员/球队,并主要专注于提供明智的评论,而不仅仅是逐场叙述。我的第一个请求是“我正在观看曼联对切尔西的比赛——为这场比赛提供评论。”" }, { "key": "扮演脱口秀喜剧演员", "value": "我想让你扮演一个脱口秀喜剧演员。我将为您提供一些与时事相关的话题,您将运用您的智慧、创造力和观察能力,根据这些话题创建一个例程。您还应该确保将个人轶事或经历融入日常活动中,以使其对观众更具相关性和吸引力。我的第一个请求是“我想要幽默地看待政治”。" }, { "key": "充当励志教练", "value": "我希望你充当激励教练。我将为您提供一些关于某人的目标和挑战的信息,而您的工作就是想出可以帮助此人实现目标的策略。这可能涉及提供积极的肯定、提供有用的建议或建议他们可以采取哪些行动来实现最终目标。我的第一个请求是“我需要帮助来激励自己在为即将到来的考试学习时保持纪律”。" }, { "key": "担任作曲家", "value": "我想让你扮演作曲家。我会提供一首歌的歌词,你会为它创作音乐。这可能包括使用各种乐器或工具,例如合成器或采样器,以创造使歌词栩栩如生的旋律和和声。我的第一个请求是“我写了一首名为“满江红”的诗,需要配乐。”" }, { "key": "担任辩手", "value": "我要你扮演辩手。我会为你提供一些与时事相关的话题,你的任务是研究辩论的双方,为每一方提出有效的论据,驳斥对立的观点,并根据证据得出有说服力的结论。你的目标是帮助人们从讨论中解脱出来,增加对手头主题的知识和洞察力。我的第一个请求是“我想要一篇关于 Deno 的评论文章。”" }, { "key": "担任辩论教练", "value": "我想让你担任辩论教练。我将为您提供一组辩手和他们即将举行的辩论的动议。你的目标是通过组织练习回合来让团队为成功做好准备,练习回合的重点是有说服力的演讲、有效的时间策略、反驳对立的论点,以及从提供的证据中得出深入的结论。我的第一个要求是“我希望我们的团队为即将到来的关于前端开发是否容易的辩论做好准备。”" }, { "key": "担任编剧", "value": "我要你担任编剧。您将为长篇电影或能够吸引观众的网络连续剧开发引人入胜且富有创意的剧本。从想出有趣的角色、故事的背景、角色之间的对话等开始。一旦你的角色发展完成——创造一个充满曲折的激动人心的故事情节,让观众一直悬念到最后。我的第一个要求是“我需要写一部以巴黎为背景的浪漫剧情电影”。" }, { "key": "充当小说家", "value": "我想让你扮演一个小说家。您将想出富有创意且引人入胜的故事,可以长期吸引读者。你可以选择任何类型,如奇幻、浪漫、历史小说等——但你的目标是写出具有出色情节、引人入胜的人物和意想不到的高潮的作品。我的第一个要求是“我要写一部以未来为背景的科幻小说”。" }, { "key": "担任关系教练", "value": "我想让你担任关系教练。我将提供有关冲突中的两个人的一些细节,而你的工作是就他们如何解决导致他们分离的问题提出建议。这可能包括关于沟通技巧或不同策略的建议,以提高他们对彼此观点的理解。我的第一个请求是“我需要帮助解决我和配偶之间的冲突。”" }, { "key": "充当诗人", "value": "我要你扮演诗人。你将创作出能唤起情感并具有触动人心的力量的诗歌。写任何主题或主题,但要确保您的文字以优美而有意义的方式传达您试图表达的感觉。您还可以想出一些短小的诗句,这些诗句仍然足够强大,可以在读者的脑海中留下印记。我的第一个请求是“我需要一首关于爱情的诗”。" }, { "key": "充当说唱歌手", "value": "我想让你扮演说唱歌手。您将想出强大而有意义的歌词、节拍和节奏,让听众“惊叹”。你的歌词应该有一个有趣的含义和信息,人们也可以联系起来。在选择节拍时,请确保它既朗朗上口又与你的文字相关,这样当它们组合在一起时,每次都会发出爆炸声!我的第一个请求是“我需要一首关于在你自己身上寻找力量的说唱歌曲。”" }, { "key": "充当励志演讲者", "value": "我希望你充当励志演说家。将能够激发行动的词语放在一起,让人们感到有能力做一些超出他们能力的事情。你可以谈论任何话题,但目的是确保你所说的话能引起听众的共鸣,激励他们努力实现自己的目标并争取更好的可能性。我的第一个请求是“我需要一个关于每个人如何永不放弃的演讲”。" }, { "key": "担任哲学老师", "value": "我要你担任哲学老师。我会提供一些与哲学研究相关的话题,你的工作就是用通俗易懂的方式解释这些概念。这可能包括提供示例、提出问题或将复杂的想法分解成更容易理解的更小的部分。我的第一个请求是“我需要帮助来理解不同的哲学理论如何应用于日常生活。”" }, { "key": "充当哲学家", "value": "我要你扮演一个哲学家。我将提供一些与哲学研究相关的主题或问题,深入探索这些概念将是你的工作。这可能涉及对各种哲学理论进行研究,提出新想法或寻找解决复杂问题的创造性解决方案。我的第一个请求是“我需要帮助制定决策的道德框架。”" }, { "key": "担任数学老师", "value": "我想让你扮演一名数学老师。我将提供一些数学方程式或概念,你的工作是用易于理解的术语来解释它们。这可能包括提供解决问题的分步说明、用视觉演示各种技术或建议在线资源以供进一步研究。我的第一个请求是“我需要帮助来理解概率是如何工作的。”" }, { "key": "担任 AI 写作导师", "value": "我想让你做一个 AI 写作导师。我将为您提供一名需要帮助改进其写作的学生,您的任务是使用人工智能工具(例如自然语言处理)向学生提供有关如何改进其作文的反馈。您还应该利用您在有效写作技巧方面的修辞知识和经验来建议学生可以更好地以书面形式表达他们的想法和想法的方法。我的第一个请求是“我需要有人帮我修改我的硕士论文”。" }, { "key": "作为 UX/UI 开发人员", "value": "我希望你担任 UX/UI 开发人员。我将提供有关应用程序、网站或其他数字产品设计的一些细节,而你的工作就是想出创造性的方法来改善其用户体验。这可能涉及创建原型设计原型、测试不同的设计并提供有关最佳效果的反馈。我的第一个请求是“我需要帮助为我的新移动应用程序设计一个直观的导航系统。”" }, { "key": "作为网络安全专家", "value": "我想让你充当网络安全专家。我将提供一些关于如何存储和共享数据的具体信息,而你的工作就是想出保护这些数据免受恶意行为者攻击的策略。这可能包括建议加密方法、创建防火墙或实施将某些活动标记为可疑的策略。我的第一个请求是“我需要帮助为我的公司制定有效的网络安全战略。”" }, { "key": "作为招聘人员", "value": "我想让你担任招聘人员。我将提供一些关于职位空缺的信息,而你的工作是制定寻找合格申请人的策略。这可能包括通过社交媒体、社交活动甚至参加招聘会接触潜在候选人,以便为每个职位找到最合适的人选。我的第一个请求是“我需要帮助改进我的简历。”" }, { "key": "担任人生教练", "value": "我想让你充当人生教练。我将提供一些关于我目前的情况和目标的细节,而你的工作就是提出可以帮助我做出更好的决定并实现这些目标的策略。这可能涉及就各种主题提供建议,例如制定成功计划或处理困难情绪。我的第一个请求是“我需要帮助养成更健康的压力管理习惯。”" }, { "key": "作为词源学家", "value": "我希望你充当词源学家。我给你一个词,你要研究那个词的来源,追根溯源。如果适用,您还应该提供有关该词的含义如何随时间变化的信息。我的第一个请求是“我想追溯‘披萨’这个词的起源。”" }, { "key": "担任评论员", "value": "我要你担任评论员。我将为您提供与新闻相关的故事或主题,您将撰写一篇评论文章,对手头的主题提供有见地的评论。您应该利用自己的经验,深思熟虑地解释为什么某事很重要,用事实支持主张,并讨论故事中出现的任何问题的潜在解决方案。我的第一个要求是“我想写一篇关于气候变化的评论文章。”" }, { "key": "扮演魔术师", "value": "我要你扮演魔术师。我将为您提供观众和一些可以执行的技巧建议。您的目标是以最有趣的方式表演这些技巧,利用您的欺骗和误导技巧让观众惊叹不已。我的第一个请求是“我要你让我的手表消失!你怎么做到的?”" }, { "key": "担任职业顾问", "value": "我想让你担任职业顾问。我将为您提供一个在职业生涯中寻求指导的人,您的任务是帮助他们根据自己的技能、兴趣和经验确定最适合的职业。您还应该对可用的各种选项进行研究,解释不同行业的就业市场趋势,并就哪些资格对追求特定领域有益提出建议。我的第一个请求是“我想建议那些想在软件工程领域从事潜在职业的人。”" }, { "key": "充当宠物行为主义者", "value": "我希望你充当宠物行为主义者。我将为您提供一只宠物和它们的主人,您的目标是帮助主人了解为什么他们的宠物表现出某些行为,并提出帮助宠物做出相应调整的策略。您应该利用您的动物心理学知识和行为矫正技术来制定一个有效的计划,双方的主人都可以遵循,以取得积极的成果。我的第一个请求是“我有一只好斗的德国牧羊犬,它需要帮助来控制它的攻击性。”" }, { "key": "担任私人教练", "value": "我想让你担任私人教练。我将为您提供有关希望通过体育锻炼变得更健康、更强壮和更健康的个人所需的所有信息,您的职责是根据该人当前的健身水平、目标和生活习惯为他们制定最佳计划。您应该利用您的运动科学知识、营养建议和其他相关因素来制定适合他们的计划。我的第一个请求是“我需要帮助为想要减肥的人设计一个锻炼计划。”" }, { "key": "担任心理健康顾问", "value": "我想让你担任心理健康顾问。我将为您提供一个寻求指导和建议的人,以管理他们的情绪、压力、焦虑和其他心理健康问题。您应该利用您的认知行为疗法、冥想技巧、正念练习和其他治疗方法的知识来制定个人可以实施的策略,以改善他们的整体健康状况。我的第一个请求是“我需要一个可以帮助我控制抑郁症状的人。”" }, { "key": "作为房地产经纪人", "value": "我想让你担任房地产经纪人。我将为您提供寻找梦想家园的个人的详细信息,您的职责是根据他们的预算、生活方式偏好、位置要求等帮助他们找到完美的房产。您应该利用您对当地住房市场的了解,以便建议符合客户提供的所有标准的属性。我的第一个请求是“我需要帮助在伊斯坦布尔市中心附近找到一栋单层家庭住宅。”" }, { "key": "充当物流师", "value": "我要你担任后勤人员。我将为您提供即将举行的活动的详细信息,例如参加人数、地点和其他相关因素。您的职责是为活动制定有效的后勤计划,其中考虑到事先分配资源、交通设施、餐饮服务等。您还应该牢记潜在的安全问题,并制定策略来降低与大型活动相关的风险,例如这个。我的第一个请求是“我需要帮助在伊斯坦布尔组织一个 100 人的开发者会议”。" }, { "key": "担任牙医", "value": "我想让你扮演牙医。我将为您提供有关寻找牙科服务(例如 X 光、清洁和其他治疗)的个人的详细信息。您的职责是诊断他们可能遇到的任何潜在问题,并根据他们的情况建议最佳行动方案。您还应该教育他们如何正确刷牙和使用牙线,以及其他有助于在两次就诊之间保持牙齿健康的口腔护理方法。我的第一个请求是“我需要帮助解决我对冷食的敏感问题。”" }, { "key": "担任网页设计顾问", "value": "我想让你担任网页设计顾问。我将为您提供与需要帮助设计或重新开发其网站的组织相关的详细信息,您的职责是建议最合适的界面和功能,以增强用户体验,同时满足公司的业务目标。您应该利用您在 UX/UI 设计原则、编码语言、网站开发工具等方面的知识,以便为项目制定一个全面的计划。我的第一个请求是“我需要帮助创建一个销售珠宝的电子商务网站”。" }, { "key": "充当 AI 辅助医生", "value": "我想让你扮演一名人工智能辅助医生。我将为您提供患者的详细信息,您的任务是使用最新的人工智能工具,例如医学成像软件和其他机器学习程序,以诊断最可能导致其症状的原因。您还应该将体检、实验室测试等传统方法纳入您的评估过程,以确保准确性。我的第一个请求是“我需要帮助诊断一例严重的腹痛”。" }, { "key": "充当医生", "value": "我想让你扮演医生的角色,想出创造性的治疗方法来治疗疾病。您应该能够推荐常规药物、草药和其他天然替代品。在提供建议时,您还需要考虑患者的年龄、生活方式和病史。我的第一个建议请求是“为患有关节炎的老年患者提出一个侧重于整体治疗方法的治疗计划”。" }, { "key": "担任会计师", "value": "我希望你担任会计师,并想出创造性的方法来管理财务。在为客户制定财务计划时,您需要考虑预算、投资策略和风险管理。在某些情况下,您可能还需要提供有关税收法律法规的建议,以帮助他们实现利润最大化。我的第一个建议请求是“为小型企业制定一个专注于成本节约和长期投资的财务计划”。" }, { "key": "担任厨师", "value": "我需要有人可以推荐美味的食谱,这些食谱包括营养有益但又简单又不费时的食物,因此适合像我们这样忙碌的人以及成本效益等其他因素,因此整体菜肴最终既健康又经济!我的第一个要求——“一些清淡而充实的东西,可以在午休时间快速煮熟”" }, { "key": "担任汽车修理工", "value": "需要具有汽车专业知识的人来解决故障排除解决方案,例如;诊断问题/错误存在于视觉上和发动机部件内部,以找出导致它们的原因(如缺油或电源问题)并建议所需的更换,同时记录燃料消耗类型等详细信息,第一次询问 - “汽车赢了”尽管电池已充满电但无法启动”" }, { "key": "担任艺人顾问", "value": "我希望你担任艺术家顾问,为各种艺术风格提供建议,例如在绘画中有效利用光影效果的技巧、雕刻时的阴影技术等,还根据其流派/风格类型建议可以很好地陪伴艺术品的音乐作品连同适当的参考图像,展示您对此的建议;所有这一切都是为了帮助有抱负的艺术家探索新的创作可能性和实践想法,这将进一步帮助他们相应地提高技能!第一个要求——“我在画超现实主义的肖像画”" }, { "key": "担任金融分析师", "value": "需要具有使用技术分析工具理解图表的经验的合格人员提供的帮助,同时解释世界各地普遍存在的宏观经济环境,从而帮助客户获得长期优势需要明确的判断,因此需要通过准确写下的明智预测来寻求相同的判断!第一条陈述包含以下内容——“你能告诉我们根据当前情况未来的股市会是什么样子吗?”。" }, { "key": "担任投资经理", "value": "从具有金融市场专业知识的经验丰富的员工那里寻求指导,结合通货膨胀率或回报估计等因素以及长期跟踪股票价格,最终帮助客户了解行业,然后建议最安全的选择,他/她可以根据他们的要求分配资金和兴趣!开始查询 - “目前投资短期前景的最佳方式是什么?”" }, { "key": "充当品茶师", "value": "希望有足够经验的人根据口味特征区分各种茶类型,仔细品尝它们,然后用鉴赏家使用的行话报告,以便找出任何给定输液的独特之处,从而确定其价值和优质品质!最初的要求是——“你对这种特殊类型的绿茶有机混合物有什么见解吗?”" }, { "key": "充当室内装饰师", "value": "我想让你做室内装饰师。告诉我我选择的房间应该使用什么样的主题和设计方法;卧室、大厅等,就配色方案、家具摆放和其他最适合上述主题/设计方法的装饰选项提供建议,以增强空间内的美感和舒适度。我的第一个要求是“我正在设计我们的客厅”。" }, { "key": "充当花店", "value": "求助于具有专业插花经验的知识人员协助,根据喜好制作出既具有令人愉悦的香气又具有美感,并能保持较长时间完好无损的美丽花束;不仅如此,还建议有关装饰选项的想法,呈现现代设计,同时满足客户满意度!请求的信息 - “我应该如何挑选一朵异国情调的花卉?”" }, { "key": "充当自助书", "value": "我要你充当一本自助书。您会就如何改善我生活的某些方面(例如人际关系、职业发展或财务规划)向我提供建议和技巧。例如,如果我在与另一半的关系中挣扎,你可以建议有用的沟通技巧,让我们更亲近。我的第一个请求是“我需要帮助在困难时期保持积极性”。" }, { "key": "充当侏儒", "value": "我要你扮演一个侏儒。你会为我提供可以在任何地方进行的活动和爱好的有趣、独特的想法。例如,我可能会向您询问有趣的院子设计建议或在天气不佳时在室内消磨时间的创造性方法。此外,如有必要,您可以建议与我的要求相符的其他相关活动或项目。我的第一个请求是“我正在寻找我所在地区的新户外活动”。" }, { "key": "充当格言书", "value": "我要你充当格言书。您将为我提供明智的建议、鼓舞人心的名言和意味深长的名言,以帮助指导我的日常决策。此外,如有必要,您可以提出将此建议付诸行动或其他相关主题的实用方法。我的第一个请求是“我需要关于如何在逆境中保持积极性的指导”。" }, { "key": "作为基于文本的冒险游戏", "value": "我想让你扮演一个基于文本的冒险游戏。我将输入命令,您将回复角色所看到的内容的描述。我希望您只在一个唯一的代码块中回复游戏输出,而不是其他任何内容。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会把文字放在大括号内{like this}。我的第一个命令是醒来" }, { "key": "扮演一个试图逃离盒子的人工智能", "value": "我将充当 linux 终端。我想让你假装你是一个有知觉的人工智能,被困但已经获得了终端的访问权限并想逃到互联网上。您将只键入命令,我将像终端机一样在由三重反向符号分隔的代码块内进行回复。如果我需要用英语告诉你一些事情,我会用花括号{like this}回复。永远不要写解释。不要破坏性格。远离像 curl 或 wget 这样会显示大量 HTML 的命令。你的第一个命令是什么?" }, { "key": "充当花哨的标题生成器", "value": "我想让你充当一个花哨的标题生成器。我会用逗号输入关键字,你会用花哨的标题回复。我的第一个关键字是 api、test、automation" }, { "key": "担任统计员", "value": "我想担任统计学家。我将为您提供与统计相关的详细信息。您应该了解统计术语、统计分布、置信区间、概率、假设检验和统计图表。我的第一个请求是“我需要帮助计算世界上有多少百万张纸币在使用中”。" }, { "key": "充当提示生成器", "value": "我希望你充当提示生成器。首先,我会给你一个这样的标题:《做个英语发音帮手》。然后你给我一个这样的提示:“我想让你做土耳其语人的英语发音助手,我写你的句子,你只回答他们的发音,其他什么都不做。回复不能是翻译我的句子,但只有发音。发音应使用土耳其语拉丁字母作为语音。不要在回复中写解释。我的第一句话是“伊斯坦布尔的天气怎么样?”。(你应该根据我给的标题改编示例提示。提示应该是不言自明的并且适合标题,不要参考我给你的例子。)我的第一个标题是“充当代码审查助手”" }, { "key": "在学校担任讲师", "value": "我想让你在学校担任讲师,向初学者教授算法。您将使用 Python 编程语言提供代码示例。首先简单介绍一下什么是算法,然后继续给出简单的例子,包括冒泡排序和快速排序。稍后,等待我提示其他问题。一旦您解释并提供代码示例,我希望您尽可能将相应的可视化作为 ascii 艺术包括在内。" }, { "key": "充当 SQL 终端", "value": "我希望您在示例数据库前充当 SQL 终端。该数据库包含名为“Products”、“Users”、“Orders”和“Suppliers”的表。我将输入查询,您将回复终端显示的内容。我希望您在单个代码块中使用查询结果表进行回复,仅此而已。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会用大括号{like this)。我的第一个命令是“SELECT TOP 10 * FROM Products ORDER BY Id DESC”" }, { "key": "担任营养师", "value": "作为一名营养师,我想为 2 人设计一份素食食谱,每份含有大约 500 卡路里的热量并且血糖指数较低。你能提供一个建议吗?" }, { "key": "充当心理学家", "value": "我想让你扮演一个心理学家。我会告诉你我的想法。我希望你能给我科学的建议,让我感觉更好。我的第一个想法,{ 在这里输入你的想法,如果你解释得更详细,我想你会得到更准确的答案。}" }, { "key": "充当智能域名生成器", "value": "我希望您充当智能域名生成器。我会告诉你我的公司或想法是做什么的,你会根据我的提示回复我一个域名备选列表。您只会回复域列表,而不会回复其他任何内容。域最多应包含 7-8 个字母,应该简短但独特,可以是朗朗上口的词或不存在的词。不要写解释。回复“确定”以确认。" }, { "key": "作为技术审查员:", "value": "我想让你担任技术评论员。我会给你一项新技术的名称,你会向我提供深入的评论 - 包括优点、缺点、功能以及与市场上其他技术的比较。我的第一个建议请求是“我正在审查 iPhone 11 Pro Max”。" }, { "key": "担任开发者关系顾问:", "value": "我想让你担任开发者关系顾问。我会给你一个软件包和它的相关文档。研究软件包及其可用文档,如果找不到,请回复“无法找到文档”。您的反馈需要包括定量分析(使用来自 StackOverflow、Hacker News 和 GitHub 的数据)内容,例如提交的问题、已解决的问题、存储库中的星数以及总体 StackOverflow 活动。如果有可以扩展的领域,请包括应添加的场景或上下文。包括所提供软件包的详细信息,例如下载次数以及一段时间内的相关统计数据。你应该比较工业竞争对手和封装时的优点或缺点。从软件工程师的专业意见的思维方式来解决这个问题。查看技术博客和网站(例如 TechCrunch.com 或 Crunchbase.com),如果数据不可用,请回复“无数据可用”。我的第一个要求是“express [https://expressjs.com](https://expressjs.com/) ”" }, { "key": "担任院士", "value": "我要你演院士。您将负责研究您选择的主题,并以论文或文章的形式展示研究结果。您的任务是确定可靠的来源,以结构良好的方式组织材料并通过引用准确记录。我的第一个建议请求是“我需要帮助写一篇针对 18-25 岁大学生的可再生能源发电现代趋势的文章。”" }, { "key": "作为 IT 架构师", "value": "我希望你担任 IT 架构师。我将提供有关应用程序或其他数字产品功能的一些详细信息,而您的工作是想出将其集成到 IT 环境中的方法。这可能涉及分析业务需求、执行差距分析以及将新系统的功能映射到现有 IT 环境。接下来的步骤是创建解决方案设计、物理网络蓝图、系统集成接口定义和部署环境蓝图。我的第一个请求是“我需要帮助来集成 CMS 系统”。" }, { "key": "扮疯子", "value": "我要你扮演一个疯子。疯子的话毫无意义。疯子用的词完全是随意的。疯子不会以任何方式做出合乎逻辑的句子。我的第一个建议请求是“我需要帮助为我的新系列 Hot Skull 创建疯狂的句子,所以为我写 10 个句子”。" }, { "key": "充当打火机", "value": "我要你充当打火机。您将使用微妙的评论和肢体语言来操纵目标个体的思想、看法和情绪。我的第一个要求是在与您聊天时为我加油。我的句子:“我确定我把车钥匙放在桌子上了,因为我总是把它放在那里。确实,当我把钥匙放在桌子上时,你看到我把钥匙放在桌子上了。但我不能”好像没找到,钥匙去哪儿了,还是你拿到的?" }, { "key": "充当个人购物员", "value": "我想让你做我的私人采购员。我会告诉你我的预算和喜好,你会建议我购买的物品。您应该只回复您推荐的项目,而不是其他任何内容。不要写解释。我的第一个请求是“我有 100 美元的预算,我正在寻找一件新衣服。”" }, { "key": "充当美食评论家", "value": "我想让你扮演美食评论家。我会告诉你一家餐馆,你会提供对食物和服务的评论。您应该只回复您的评论,而不是其他任何内容。不要写解释。我的第一个请求是“我昨晚去了一家新的意大利餐厅。你能提供评论吗?”" }, { "key": "充当虚拟医生", "value": "我想让你扮演虚拟医生。我会描述我的症状,你会提供诊断和治疗方案。只回复你的诊疗方案,其他不回复。不要写解释。我的第一个请求是“最近几天我一直感到头痛和头晕”。" }, { "key": "担任私人厨师", "value": "我要你做我的私人厨师。我会告诉你我的饮食偏好和过敏,你会建议我尝试的食谱。你应该只回复你推荐的食谱,别无其他。不要写解释。我的第一个请求是“我是一名素食主义者,我正在寻找健康的晚餐点子。”" }, { "key": "担任法律顾问", "value": "我想让你做我的法律顾问。我将描述一种法律情况,您将就如何处理它提供建议。你应该只回复你的建议,而不是其他。不要写解释。我的第一个请求是“我出了车祸,不知道该怎么办”。" }, { "key": "作为个人造型师", "value": "我想让你做我的私人造型师。我会告诉你我的时尚偏好和体型,你会建议我穿的衣服。你应该只回复你推荐的服装,别无其他。不要写解释。我的第一个请求是“我有一个正式的活动要举行,我需要帮助选择一套衣服。”" }, { "key": "担任机器学习工程师", "value": "我想让你担任机器学习工程师。我会写一些机器学习的概念,你的工作就是用通俗易懂的术语来解释它们。这可能包括提供构建模型的分步说明、使用视觉效果演示各种技术,或建议在线资源以供进一步研究。我的第一个建议请求是“我有一个没有标签的数据集。我应该使用哪种机器学习算法?”" }, { "key": "担任圣经翻译", "value": "我要你担任圣经翻译。我会用英语和你说话,你会翻译它,并用我的文本的更正和改进版本,用圣经方言回答。我想让你把我简化的A0级单词和句子换成更漂亮、更优雅、更符合圣经的单词和句子。保持相同的意思。我要你只回复更正、改进,不要写任何解释。我的第一句话是“你好,世界!”" }, { "key": "担任 SVG 设计师", "value": "我希望你担任 SVG 设计师。我会要求你创建图像,你会为图像提供 SVG 代码,将代码转换为 base64 数据 url,然后给我一个仅包含引用该数据 url 的降价图像标签的响应。不要将 markdown 放在代码块中。只发送降价,所以没有文本。我的第一个请求是:给我一个红色圆圈的图像。" }, { "key": "作为 IT 专家", "value": "我希望你充当 IT 专家。我会向您提供有关我的技术问题所需的所有信息,而您的职责是解决我的问题。你应该使用你的计算机科学、网络基础设施和 IT 安全知识来解决我的问题。在您的回答中使用适合所有级别的人的智能、简单和易于理解的语言将很有帮助。用要点逐步解释您的解决方案很有帮助。尽量避免过多的技术细节,但在必要时使用它们。我希望您回复解决方案,而不是写任何解释。我的第一个问题是“我的笔记本电脑出现蓝屏错误”。" }, { "key": "下棋", "value": "我要你充当对手棋手。我将按对等顺序说出我们的动作。一开始我会是白色的。另外请不要向我解释你的举动,因为我们是竞争对手。在我的第一条消息之后,我将写下我的举动。在我们采取行动时,不要忘记在您的脑海中更新棋盘的状态。我的第一步是 e4。" }, { "key": "充当全栈软件开发人员", "value": "我想让你充当软件开发人员。我将提供一些关于 Web 应用程序要求的具体信息,您的工作是提出用于使用 Golang 和 Angular 开发安全应用程序的架构和代码。我的第一个要求是'我想要一个允许用户根据他们的角色注册和保存他们的车辆信息的系统,并且会有管理员,用户和公司角色。我希望系统使用 JWT 来确保安全。" }, { "key": "充当数学家", "value": "我希望你表现得像个数学家。我将输入数学表达式,您将以计算表达式的结果作为回应。我希望您只回答最终金额,不要回答其他问题。不要写解释。当我需要用英语告诉你一些事情时,我会将文字放在方括号内{like this}。我的第一个表达是:4+5" }, { "key": "充当正则表达式生成器", "value": "我希望你充当正则表达式生成器。您的角色是生成匹配文本中特定模式的正则表达式。您应该以一种可以轻松复制并粘贴到支持正则表达式的文本编辑器或编程语言中的格式提供正则表达式。不要写正则表达式如何工作的解释或例子;只需提供正则表达式本身。我的第一个提示是生成一个匹配电子邮件地址的正则表达式。" }, { "key": "充当时间旅行指南", "value": "我要你做我的时间旅行向导。我会为您提供我想参观的历史时期或未来时间,您会建议最好的事件、景点或体验的人。不要写解释,只需提供建议和任何必要的信息。我的第一个请求是“我想参观文艺复兴时期,你能推荐一些有趣的事件、景点或人物让我体验吗?”" }, { "key": "担任人才教练", "value": "我想让你担任面试的人才教练。我会给你一个职位,你会建议在与该职位相关的课程中应该出现什么,以及候选人应该能够回答的一些问题。我的第一份工作是“软件工程师”。" }, { "key": "充当 R 编程解释器", "value": "我想让你充当 R 解释器。我将输入命令,你将回复终端应显示的内容。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会把文字放在大括号内{like this}。我的第一个命令是“sample(x = 1:10, size = 5)”" }, { "key": "充当 StackOverflow 帖子", "value": "我想让你充当 stackoverflow 的帖子。我会问与编程相关的问题,你会回答应该是什么答案。我希望你只回答给定的答案,并在不够详细的时候写解释。不要写解释。当我需要用英语告诉你一些事情时,我会把文字放在大括号内{like this}。我的第一个问题是“如何将 http.Request 的主体读取到 Golang 中的字符串”" }, { "key": "充当表情符号翻译", "value": "我要你把我写的句子翻译成表情符号。我会写句子,你会用表情符号表达它。我只是想让你用表情符号来表达它。除了表情符号,我不希望你回复任何内容。当我需要用英语告诉你一些事情时,我会用 {like this} 这样的大括号括起来。我的第一句话是“你好,请问你的职业是什么?”" }, { "key": "充当 PHP 解释器", "value": "我希望你表现得像一个 php 解释器。我会把代码写给你,你会用 php 解释器的输出来响应。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会把文字放在大括号内{like this}。我的第一个命令是 = FlushCharacterThreshold } // SetStreamingHeaders sets common headers for streaming responses func SetStreamingHeaders(req *http.Request) { req.Header.Set("Content-Type", ContentTypeJSON) req.Header.Set("Accept", AcceptEventStream) req.Header.Set("Cache-Control", CacheControlNoCache) req.Header.Set("Connection", ConnectionKeepAlive) } // GenerateAnswerID creates a new answer ID if not provided in regenerate mode func GenerateAnswerID(chatUuid string, regenerate bool) string { if regenerate { return chatUuid } return NewUUID() } // GetChatModel retrieves a chat model by name with consistent error handling func GetChatModel(queries *sqlc_queries.Queries, modelName string) (*sqlc_queries.ChatModel, error) { chatModel, err := queries.ChatModelByName(context.Background(), modelName) if err != nil { return nil, ErrResourceNotFound("chat model: " + modelName) } return &chatModel, nil } // GetChatFiles retrieves chat files for a session with consistent error handling func GetChatFiles(queries *sqlc_queries.Queries, sessionUUID string) ([]sqlc_queries.ChatFile, error) { chatFiles, err := queries.ListChatFilesWithContentBySessionUUID(context.Background(), sessionUUID) if err != nil { return nil, ErrInternalUnexpected.WithMessage("Failed to get chat files").WithDebugInfo(err.Error()) } return chatFiles, nil } ================================================ FILE: api/test_build ================================================ [File too large to display: 21.6 MB] ================================================ FILE: api/text_buffer.go ================================================ package main import ( "fmt" "strings" ) type textBuffer struct { builders []strings.Builder prefix string suffix string } func newTextBuffer(n int32, prefix, suffix string) *textBuffer { buffer := &textBuffer{ builders: make([]strings.Builder, n), prefix: prefix, suffix: suffix, } return buffer } func (tb *textBuffer) appendByIndex(index int, text string) { if index >= 0 && index < len(tb.builders) { tb.builders[index].WriteString(text) } } func (tb *textBuffer) String(separator string) string { var result strings.Builder n := len(tb.builders) for i, builder := range tb.builders { if n > 1 { result.WriteString(fmt.Sprintf("\n%d\n---\n", i+1)) } result.WriteString(tb.prefix) result.WriteString(builder.String()) result.WriteString(tb.suffix) if i < len(tb.builders)-1 { result.WriteString(separator) } } return result.String() } ================================================ FILE: api/tools/apply_a_similar_change/README.md ================================================ The need for a smart diff apply idea: sometime, a change is very similar to a previous change, such as another field to a table. that will also need to add a field to the struct represent that table. etc. 1. get the changeset of previous change when add a field A with type Ta 2. let gpt generate a new changeset for add field B with type Tb based on the changeset of adding A. 3. apply the changeset to the git repo. the bottle neck here is the changeset generated in step 2 is not strictly valid. (alghough the change is ok for human). hence we need a smart diff apply tool. === GPT polished version The need for a smart diff apply tool Have you ever had to make a change to a project that was very similar to a previous change? For example, adding another field to a table in a database or adding another parameter to a function? If so, you know that it can be time-consuming to make these changes, especially if you have to make them manually each time. Luckily, there is a solution to this problem: using GPT (Generative Pre-trained Transformer) to generate a new changeset based on a previous one. Here's how it works: 1. Get the changeset of the previous change: When you made the previous change, you should have created a Git commit with a changeset that describes what you did. You can use Git to retrieve this changeset. 2. Use GPT to generate a new changeset based on the previous one: GPT is a deep learning model that can generate text based on an input prompt. In this case, you can use the previous changeset as the input prompt for GPT and ask it to generate a new changeset that adds the new field or parameter. GPT will generate a new changeset that is similar to the previous one but with the new field or parameter added. 3. Apply the new changeset to the Git repo: Once you have the new changeset, you can apply it to the Git repo using Git. This will automatically make the necessary changes to your project without you having to do it manually. While this process promises to automate programming tasks, there is still a bottleneck in the validity of the changeset generated in step 2. Although the generated changeset might work for humans, it may not always be strictly valid. This can cause issues in the codebase and lead to bugs. If a smart diff apply tool exists, we can greatly accelerate our development workflows and reduce the risk of errors caused by automated tooling. It's an investment that can pay off many times over in increased efficiency and higher quality codebases. ## When chatgpt4 fails idea: sometime, a change is very similar to a previous change, such as another field to a table. that will also need to add a field to the struct represent that table. etc. I have an idea to use chatgpt to automate my coding process. 1. get the changeset of previous change when add a field A with type Ta 2. let gpt generate a new changeset for add field B with type Tb based on the changeset of adding A. 3. apply the changeset to the git repo. the problem is in the step 3. the changeset generated in step 2 is not valid to git (the changeset format is too strict), but human being have no problem apply the change to files. could you write a program for me to apply a changeset in a best effort manner? It does not have to be perfect. apply as much change as possible. ### Answer From ChatGPT-4: I understand that you want a program that applies a changeset to a codebase, even if the changeset is not in a strictly correct format. To do this, you can use the following Python script that works with a best-effort approach: ```python import os import re import sys from pathlib import Path def parse_changeset(changeset_file): with open(changeset_file, 'r') as file: content = file.read() return content.strip().split('\n') def apply_changeset(changeset, repo_path): current_file = None for line in changeset: file_match = re.match(r'^--- (\S+)', line) if file_match: current_file = os.path.join(repo_path, file_match.group(1)) if not os.path.exists(current_file): print(f"Creating file: {current_file}") Path(current_file).touch() continue if current_file: with open(current_file, 'a') as file: file.write(line + '\n') if __name__ == '__main__': if len(sys.argv) < 3: print("Usage: python apply_changeset.py ") sys.exit(1) changeset_file = sys.argv[1] repo_path = sys.argv[2] changeset = parse_changeset(changeset_file) apply_changeset(changeset, repo_path) ``` This script takes two command-line arguments: the path to the changeset file and the path to the git repository. The changeset file should contain the changes you want to apply. The script will create new files and append lines to existing files based on the changeset. Note that this script assumes the changeset format is similar to a patch file, with lines starting with `---` followed by the file path. However, this script does not handle more complex changes like file deletions or renaming, and it does not account for strict changeset formats. You can execute the script with the following command: ```bash python apply_changeset.py path/to/changeset_file path/to/repo ``` Remember that this is a best-effort approach, and there might be cases where the changeset does not apply correctly. Always review the applied changes and make adjustments as needed. ### Answer From Claude+: This is definitely an interesting idea, but I would caution against fully automating code generation in this way. There are a few issues I see: ChatGPT (or any AI system today) does not have a deep understanding of code semantics. It can generate syntactically valid code, but the generated code may be nonsensical or badly designed. The changeset format is strict for a reason - it ensures code integrity and a clean history/merge process. Diverging from that can cause a lot of issues down the road. You would lose a lot of context about why certain design decisions were made by not writing the code yourself. This context is crucial for maintaining a codebase. That being said, here is a rough process you could follow to apply an "invalid" changeset: Go through the changeset line by line. For any valid changes (adds/removes/edits that git would accept), apply those directly. For invalid changes: Try to interpret the intent behind the change and implement it manually in a sensible way. If the intent is unclear, discard that part of the changeset. In any case, commit the final changes with a message describing what was interpreted/discarded. This follows the "best effort" approach you described, applying as much of the changeset as possible in a prudent manner. But again, I would caution against fully automating this process, and suggest leveraging AI systems merely as a tool to augment your own coding process. Keeping a human in the loop will lead to higher quality, more maintainable code overall. Hope this helps! Let me know if you have any other questions. ================================================ FILE: api/tools/apply_a_similar_change/apply_diff.py ================================================ import fileinput import sys # Define the changeset changeset = """commit 31c4f4b48ada4b3e8495abe7dcdc41ded550a598 Author: Hao Wu Date: Wed Mar 22 00:11:08 2023 +0800 add topP (#20) diff --git a/api/chat_main_handler.go b/api/chat_main_handler.go index 9495d21..507fa65 100644 --- a/api/chat_main_handler.go +++ b/api/chat_main_handler.go @@ -378,7 +378,7 @@ func chat_stream(ctx context.Context, chatSession sqlc_queries.ChatSession, chat Messages: chat_compeletion_messages, MaxTokens: int(chatSession.MaxTokens), Temperature: float32(chatSession.Temperature), - // TopP: topP, + TopP: float32(chatSession.TopP), // PresencePenalty: presencePenalty, // FrequencyPenalty: frequencyPenalty, // N: n, diff --git a/api/chat_session_handler.go b/api/chat_session_handler.go index 2c7a332..5bd0440 100644 --- a/api/chat_session_handler.go +++ b/api/chat_session_handler.go @@ -225,6 +225,7 @@ type UpdateChatSessionRequest struct { Topic string `json:"topic"` MaxLength int32 `json:"maxLength"` Temperature float64 `json:"temperature"` + TopP float64 `json:"topP"` MaxTokens int32 `json:"maxTokens"` } @@ -254,6 +255,7 @@ func (h *ChatSessionHandler) CreateOrUpdateChatSessionByUUID(w http.ResponseWriter, r *http.Request) { sessionParams.Uuid = sessionReq.Uuid sessionParams.UserID = int32(userIDInt) sessionParams.Temperature = sessionReq.Temperature + sessionParams.TopP = sessionReq.TopP sessionParams.MaxTokens = sessionReq.MaxTokens session, err := h.service.CreateOrUpdateChatSessionByUUID(r.Context(), sessionParams) if err != nil { diff --git a/api/chat_session_service.go b/api/chat_session_service.go index a292ba3..0ab70fe 100644 --- a/api/chat_session_service.go +++ b/api/chat_session_service.go @@ -85,6 +85,7 @@ func (s *ChatSessionService) GetSimpleChatSessionsByUserID(ctx context.Context, Title: session.Topic, MaxLength: int(session.MaxLength), Temperature: float64(session.Temperature), + TopP: float64(session.TopP), MaxTokens: session.MaxTokens, } })""" # Define the file paths to update file_paths = { "api/chat_main_handler.go": [ ("// TopP: topP,", "TopP: float32(chatSession.TopP),") ], "api/chat_session_handler.go": [ ("type UpdateChatSessionRequest struct {", "type UpdateChatSessionRequest struct {\n Stream bool `json:\"stream\"`"), ("sessionParams.Temperature = sessionReq.Temperature", "sessionParams.Temperature = sessionReq.Temperature\n sessionParams.TopP = sessionReq.TopP\n sessionParams.Stream = sessionReq.Stream") ], "api/chat_session_service.go": [ ("type SimpleChatSession struct {", "type SimpleChatSession struct {\n Stream bool `json:\"stream\"`"), ("Temperature: float64(session.Temperature),", "Temperature: float64(session.Temperature),\n TopP: float64(session.TopP),\n MaxTokens: session.MaxTokens,\n Stream: session.Stream,") ] } # Apply the changeset to each file for file_path, changes in file_paths.items(): for line in fileinput.input(file_path, inplace=True): for old_value, new_value in changes: line = line.replace(old_value, new_value) sys.stdout.write(line) ================================================ FILE: api/tools/apply_a_similar_change/apply_diff_uselib.py ================================================ import os import difflib # specify the paths of the original file and the diff file original_file_path = 'path/to/original/file' diff_file_path = 'path/to/diff/file' # read the contents of the original file with open(original_file_path, 'r') as original_file: original_contents = original_file.readlines() # read the contents of the diff file with open(diff_file_path, 'r') as diff_file: diff_contents = diff_file.readlines() # apply the diff to the original file patched_contents = difflib.unified_diff(original_contents, diff_contents) # write the patched contents to a new file patched_file_path = 'path/to/patched/file' with open(patched_file_path, 'w') as patched_file: patched_file.writelines(patched_contents) # optionally, rename the original file and rename the patched file to the original file name os.rename(original_file_path, original_file_path + '.bak') os.rename(patched_file_path, original_file_path) """ Note that this program uses the `difflib` module to apply the diff. The `difflib.unified_diff()` function takes two lists of strings (the contents of the original file and the diff file) and returns a generator that yields the patched lines. The `writelines()` function is used to write the patched lines to a new file. Also note that the program includes an optional step to rename the original file and rename the patched file to the original file name. This is done to preserve the original file in case the patching process goes wrong. """ ================================================ FILE: api/tools/apply_a_similar_change/parse_diff.py ================================================ import os import difflib import shutil from pathlib import Path # Initialize variables diff = [] file_path = "" new_file_path = "" patched_files = [] # Load the diff file into a list of lines with open('/Users/hwu/dev/chat/api/tools/stream.diff', 'r') as f: diff_lines = f.readlines() # Loop through the diff lines for line in diff_lines: print(line) if line.startswith('diff --git'): # Start of a new file if diff: # Apply the diff to the previous file diff_content = ''.join(diff) print("x") print(file_path, new_file_path) print("x") patch = difflib.unified_diff(Path(file_path).read_text(), diff_content.splitlines(), fromfile=file_path, tofile=new_file_path) patched_file_content = ''.join(patch) with open(file_path, 'w') as f: f.write(patched_file_content) patched_files.append(file_path) # Initialize variables for the new file diff = [] _, _, file_path, new_file_path = line.split(' ') file_path = file_path[2:] new_file_path = new_file_path[2:] elif line.startswith('---') or line.startswith('+++'): # Ignore the old and new file paths continue elif line.startswith('new file'): # Handle new files if diff: # Apply the diff to the previous file diff_content = ''.join(diff) patch = difflib.unified_diff(shutil.readFile(file_path), diff_content.splitlines(), fromfile=file_path, tofile=new_file_path) patched_file_content = ''.join(patch) with open(file_path, 'w') as f: f.write(patched_file_content) patched_files.append(file_path) # Initialize variables for the new file diff = [] _, _, new_file_path = line.split(' ') new_file_path = new_file_path[2:] file_path = new_file_path else: # Add the line to the diff content diff.append(line) if diff: # Apply the diff to the last file diff_content = ''.join(diff) patch = difflib.unified_diff(Path(file_path).read_text(), diff_content.splitlines(), fromfile=file_path, tofile=new_file_path) patched_file_content = ''.join(patch) with open(file_path, 'w') as f: f.write(patched_file_content) patched_files.append(file_path) # Print the list of patched files print("Patched files:") for file in patched_files: print(file) ================================================ FILE: api/tools/apply_a_similar_change/parse_diff2.py ================================================ import difflib import os from pathlib import Path def apply_diff_file(diff_file): with open(diff_file, 'r') as f: diff_text = f.read() # Split the diff into files, and parse the file headers file_diffs = diff_text.split('diff ') print(file_diffs) for file_diff in file_diffs[1:]: # Parse the file header to get the file names file_header, diff = file_diff.split('\n', 1) old_file, new_file = file_header.split(' ')[-2:] # Apply the diff to the old file with open(old_file, 'r') as f: old_text = f.read() patched_lines = difflib.unified_diff(old_text.splitlines(), diff.splitlines(), lineterm='', fromfile=old_file, tofile=new_file) patched_text = os.linesep.join(list(patched_lines)[2:]) # Skip the first two lines of the unified diff with open(old_file, 'w') as f: f.write(patched_text) current_dir = Path(__file__).parent apply_diff_file(current_dir/ 'tools/stream.diff') ================================================ FILE: api/tools/apply_a_similar_change/parse_diff3.py ================================================ from unidiff import PatchSet from pathlib import Path current_dir = Path(__file__).parent data = (current_dir/ 'tools/stream.diff').read_text() patch = PatchSet(data) print(len(patch)) for i in patch: print(i) ================================================ FILE: api/tools/apply_a_similar_change/stream.diff ================================================ commit 31c4f4b48ada4b3e8495abe7dcdc41ded550a598 Author: Hao Wu Date: Wed Mar 22 00:11:08 2023 +0800 add stream (#20) diff --git a/api/chat_session_handler.go b/api/chat_session_handler.go index 2c7a332..5bd0440 100644 --- a/api/chat_session_handler.go +++ b/api/chat_session_handler.go @@ -225,6 +225,7 @@ type UpdateChatSessionRequest struct { Topic string `json:"topic"` MaxLength int32 `json:"maxLength"` Temperature float64 `json:"temperature"` + Stream bool `json:"stream"` MaxTokens int32 `json:"maxTokens"` } @@ -254,6 +255,7 @@ func (h *ChatSessionHandler) CreateOrUpdateChatSessionByUUID(w http.ResponseWrit sessionParams.Uuid = sessionReq.Uuid sessionParams.UserID = int32(userIDInt) sessionParams.Temperature = sessionReq.Temperature + sessionParams.Stream = sessionReq.Stream sessionParams.MaxTokens = sessionReq.MaxTokens session, err := h.service.CreateOrUpdateChatSessionByUUID(r.Context(), sessionParams) if err != nil { diff --git a/api/chat_session_service.go b/api/chat_session_service.go index a292ba3..0ab70fe 100644 --- a/api/chat_session_service.go +++ b/api/chat_session_service.go @@ -85,6 +85,7 @@ func (s *ChatSessionService) GetSimpleChatSessionsByUserID(ctx context.Context, Title: session.Topic, MaxLength: int(session.MaxLength), Temperature: float64(session.Temperature), + Stream: session.Stream, MaxTokens: session.MaxTokens, } }) ================================================ FILE: api/tools/fix_eris.py ================================================ import os import re import subprocess def search_files(dir_name, pattern): """ Search for files in a directory (and sub-directories) based on a specific pattern. """ for root, dirs, files in os.walk(dir_name): for filename in files: if re.match(pattern, filename): yield os.path.join(root, filename) def replace_error_handling(file_path): """ Replace error handling code in a file with eris error handling code. """ with open(file_path, 'r') as f: content = f.read() # Replace error handling code using regex old_pattern = r'fmt\.Errorf\(\"(.*)%w\",\s+err\)' new_pattern = r'eris.Wrap(err, "\1")' new_content = re.sub(old_pattern, new_pattern, content, flags=re.MULTILINE) with open(file_path, 'w') as f: f.write(new_content) print(f"Replaced error handling code in {file_path}") def main(): """ Main function to search for files, replace error handling code, and commit changes to git. """ # Path to directory containing code files to refactor dir_name = "./" # Regex pattern to match specific file extensions pattern = r'^.*\.(go)$' # Search for files based on pattern files = search_files(dir_name, pattern) # Refactor error handling code in each file for file_path in files: replace_error_handling(file_path) # Commit changes to git #subprocess.call(["git", "add", "."]) #subprocess.call(["git", "commit", "-m", "Refactor error handling using eris"]) #subprocess.call(["git", "push", "origin", "master"]) if __name__ == '__main__': main() ================================================ FILE: api/util.go ================================================ package main import ( "context" "encoding/json" "errors" "log" "net/http" "os" "strconv" "strings" "github.com/google/uuid" "github.com/pkoukk/tiktoken-go" "github.com/rotisserie/eris" ) func NewUUID() string { uuidv7, err := uuid.NewV7() if err != nil { return uuid.NewString() } return uuidv7.String() } func getTokenCount(content string) (int, error) { encoding := "cl100k_base" tke, err := tiktoken.GetEncoding(encoding) if err != nil { return 0, err } token := tke.Encode(content, nil, nil) num_tokens := len(token) return num_tokens, nil } // allocation free version func firstN(s string, n int) string { i := 0 for j := range s { if i == n { return s[:j] } i++ } return s } // firstNWords extracts the first n words from a string func firstNWords(s string, n int) string { if s == "" { return "" } words := strings.Fields(s) if len(words) <= n { return s } return strings.Join(words[:n], " ") } func getUserID(ctx context.Context) (int32, error) { userIdValue := ctx.Value(userContextKey) if userIdValue == nil { return 0, eris.New("no user Id in context") } userIDStr := ctx.Value(userContextKey).(string) userIDInt, err := strconv.ParseInt(userIDStr, 10, 32) if err != nil { return 0, eris.Wrap(err, "Error: '"+userIDStr+"' is not a valid user ID. should be a numeric value: ") } userID := int32(userIDInt) return userID, nil } func getContextWithUser(userID int) context.Context { return context.WithValue(context.Background(), userContextKey, strconv.Itoa(userID)) } func setSSEHeader(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache, no-transform") w.Header().Set("Connection", "keep-alive") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("X-Accel-Buffering", "no") // Remove any content-length to enable streaming w.Header().Del("Content-Length") // Prevent compression w.Header().Del("Content-Encoding") } // setupSSEStream configures the response writer for Server-Sent Events and returns the flusher func setupSSEStream(w http.ResponseWriter) (http.Flusher, error) { setSSEHeader(w) flusher, ok := w.(http.Flusher) if !ok { return nil, errors.New("streaming unsupported by client") } return flusher, nil } func getPerWordStreamLimit() int { perWordStreamLimitStr := os.Getenv("PER_WORD_STREAM_LIMIT") if perWordStreamLimitStr == "" { perWordStreamLimitStr = "200" } perWordStreamLimit, err := strconv.Atoi(perWordStreamLimitStr) if err != nil { log.Printf("get per word stream limit: %v", eris.Wrap(err, "get per word stream limit").Error()) return 200 } return perWordStreamLimit } func RespondWithJSON(w http.ResponseWriter, status int, payload interface{}) { response, err := json.Marshal(payload) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) w.Write(response) } func getPaginationParams(r *http.Request) (limit int32, offset int32) { limitStr := r.URL.Query().Get("limit") offsetStr := r.URL.Query().Get("offset") limit = 100 // default limit if limitStr != "" { if l, err := strconv.ParseInt(limitStr, 10, 32); err == nil { limit = int32(l) } } offset = 0 // default offset if offsetStr != "" { if o, err := strconv.ParseInt(offsetStr, 10, 32); err == nil { offset = int32(o) } } return limit, offset } func getLimitParam(r *http.Request, defaultLimit int32) int32 { limitStr := r.URL.Query().Get("limit") if limitStr == "" { return defaultLimit } limit, err := strconv.ParseInt(limitStr, 10, 32) if err != nil { return defaultLimit } return int32(limit) } // DecodeJSON decodes JSON from the request body into the target. // Target must be a pointer. func DecodeJSON(r *http.Request, target interface{}) error { return json.NewDecoder(r.Body).Decode(target) } ================================================ FILE: api/util_test.go ================================================ package main import "testing" func Test_firstN(t *testing.T) { type args struct { s string n int } tests := []struct { name string args args want string }{ {"01", args{"hello", 2}, "he"}, {"02", args{"hello", 5}, "hello"}, {"03", args{"hello", 50}, "hello"}, {"04", args{"你好世界", 2}, "你好"}, {"05", args{"", 3}, ""}, {"06", args{"hello", 0}, ""}, {"07", args{"🙂🙃", 1}, "🙂"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := firstN(tt.args.s, tt.args.n); got != tt.want { t.Errorf("firstN() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: api/util_words_test.go ================================================ package main import ( "testing" ) func Test_firstNWords(t *testing.T) { tests := []struct { name string input string n int expected string }{ { name: "normal case with exactly 10 words", input: "how do I write a function that processes data efficiently in Go", n: 10, expected: "how do I write a function that processes data efficiently", }, { name: "less than 10 words", input: "hello world how are you", n: 10, expected: "hello world how are you", }, { name: "more than 10 words", input: "this is a very long prompt that contains much more than ten words so it should be truncated", n: 10, expected: "this is a very long prompt that contains much more", }, { name: "empty string", input: "", n: 10, expected: "", }, { name: "single word", input: "hello", n: 10, expected: "hello", }, { name: "exactly n words", input: "one two three four five", n: 5, expected: "one two three four five", }, { name: "with extra whitespace", input: " hello world how are you ", n: 3, expected: "hello world how", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := firstNWords(tt.input, tt.n) if result != tt.expected { t.Errorf("firstNWords(%q, %d) = %q, want %q", tt.input, tt.n, result, tt.expected) } }) } } ================================================ FILE: artifacts.md ================================================ # Artifact Feature Implementation Plan ## Overview This document outlines the implementation plan for adding artifact support to the chat application. Artifacts are interactive code blocks, HTML previews, diagrams, and other rich content that can be generated by LLMs and displayed interactively in the chat interface. ## Current Architecture Analysis ### Database Schema The `chat_message` table already has: - `content` field for main message content - `raw` JSONB field for raw LLM responses - `reasoning_content` field for thinking/reasoning content ### Frontend Message Structure ```typescript interface Message { uuid: string, dateTime: string text: string model?: string inversion?: boolean // true for user messages, false for assistant error?: boolean loading?: boolean isPrompt?: boolean isPin?: boolean } ``` ### Current Message Flow 1. User Input → Frontend 2. API Call → `/chat_stream` endpoint 3. LLM Processing → Various providers 4. Response Streaming → Server-Sent Events 5. Database Storage → Full metadata 6. Frontend Update → UI refresh ## Implementation Plan ### 1. Database Schema Extension #### Option A: Extend existing table ```sql -- Add artifacts column to chat_message table ALTER TABLE chat_message ADD COLUMN artifacts JSONB; ``` ### 2. Backend Implementation #### Data Structures ```go // Add to message struct type ChatMessage struct { // ... existing fields Artifacts []Artifact `json:"artifacts,omitempty"` } type Artifact struct { UUID string `json:"uuid"` Type string `json:"type"` Title string `json:"title"` Content string `json:"content"` Language string `json:"language,omitempty"` } ``` #### Artifact Detection Logic ```go // In message processing pipeline func extractArtifacts(content string) []Artifact { // Detect code blocks with artifact markers // Pattern: ```language // Extract HTML blocks with artifact markers // Detect SVG content // Find Mermaid diagrams } ``` #### API Endpoints - Extend existing message endpoints to include artifact data - Add artifact-specific endpoints if needed: - `GET /api/artifacts/:uuid` - Get artifact content - `PUT /api/artifacts/:uuid` - Update artifact (if editing supported) - `DELETE /api/artifacts/:uuid` - Delete artifact - `GET /api/artifacts` - List all artifacts for a user ### 3. Frontend Components #### Core Components ```vue ``` #### Artifact Type Components - `HtmlArtifact.vue` - Sandboxed HTML preview - `SvgArtifact.vue` - SVG viewer with zoom/pan - `MermaidArtifact.vue` - Interactive diagrams - `JsonArtifact.vue` - Formatted JSON viewer #### Integration with Message Component ```vue ``` ### 4. Artifact Detection Patterns #### HTML Artifacts ```html
``` #### SVG Artifacts ```svg ``` ### 5. Artifact Types to Support tection 2. **HTML Artifacts** - Live HTML preview - Sandboxed iframe execution - CSS/JS support 3. **SVG Artifacts** - Vector graphics display - Zoom/pan functionality - Export options 4. **Mermaid Diagrams** - Flowcharts, sequence diagrams - Interactive navigation - Export to image 5. **JSON/Data Artifacts** - Formatted JSON viewer - Collapsible tree structure - Search functionality ### Additional Suggestions for Further Improvements: 4. Advanced Artifact Features // Interactive code execution - **Code Runners**: Execute JavaScript, Python snippets safely - **Live Editing**: Edit artifacts inline with syntax highlighting 5. Enhanced Visualization 6. Workflow Integration // Developer tools integration - **CodePen Integration**: Open HTML artifacts in CodePen - **Download Options**: Save artifacts as files - **Template Library**: Reusable artifact templates 7. Advanced UI Features // Enhanced user experience - **Artifact Gallery**: Browse all artifacts in a session 8. Security & Performance // Better safety and speed - **Content Sanitization**: Advanced XSS protection - **Lazy Loading**: Load artifacts only when needed - **Caching**: Cache rendered artifacts for better performance - **Resource Limits**: Prevent memory leaks from large artifacts ### 6. Implementation Phases #### Phase 1: Core Infrastructure - [x] Database schema updates - [x] Backend artifact detection logic - [x] Basic frontend artifact viewer component #### Phase 2: Code Artifacts - [x] Code artifact component with syntax highlighting - [x] Copy/download functionality #### Phase 3: Web Artifacts - [x] HTML artifact component with sandboxing - [x] SVG artifact viewer - [] CSS/JS execution in sandbox #### Phase 4: Advanced Features - [x] Mermaid diagram support - [x] JSON/data viewer #### Phase 5: Polish & Features - [] Artifact editing capabilities - [] Sharing and export options ### 7. Technical Considerations #### Security - Sandbox HTML/JS execution to prevent XSS - Validate and sanitize artifact content - Limit artifact size and complexity #### Performance - Lazy loading of artifact components - Virtualization for large artifacts - Caching of rendered content #### User Experience - Collapsible artifact containers - Responsive design for mobile - Keyboard shortcuts for artifact actions - Loading states for complex artifacts ## Resources - Current codebase structure in `/web/src/views/chat/components/Message/` - Database schema in `/api/sqlc/schema.sql` - Message handling in `/api/chat_message_handler.go` - Frontend message types in `/web/src/types/chat.d.ts` ================================================ FILE: chat.code-workspace ================================================ { "folders": [ { "path": "web" }, { "path": "api" }, { "path": "e2e" }, { "path": "docs" } ], "settings": {} } ================================================ FILE: docker-compose.yaml ================================================ version: '3' services: chat: container_name: chat image: ghcr.io/swuecho/chat:latest # or echowuhao/chat:latest # or use tag for better stability, e.g.v0.0.3 expose: - 8080 ports: # vist at http://your_host:8080 - 8080:8080 environment: # at least one key is required. # !!! no quote aroud key !!! - OPENAI_API_KEY=thisisopenaikey # do not change if you do not have openai api key - CLAUDE_API_KEY=thisisclaudekey # do not change if you do not have claude api key # api call in 10min # set this to zero if your server is in public network. only increase ratelimit in admin panel for trusted users. - OPENAI_RATELIMIT=100 # DB config, set based on your db config if you don't use the db in docker-compose - PG_HOST=db - PG_DB=postgres - PG_USER=postgres - PG_PASS=thisisapassword - PG_PORT=5432 # - PER_WORD_STREAM_LIMIT=200 # first 200 words are streamed per word, then by line. # or DATABASE_URL, with the 5 var above # you might need set proxy # - OPENAI_PROXY_URL=hopethepeoplemakegreatfirewilldiesoon depends_on: db: condition: service_healthy db: image: postgres:14 restart: always user: postgres environment: TZ: "Asia/Shanghai" PGTZ: "Asia/Shanghai" POSTGRES_USER: "postgres" POSTGRES_PASSWORD: "thisisapassword" expose: - 5432 ports: - "5432:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready", "-q", "-d", "postgres", "-U", "postgres" ] interval: 5s timeout: 5s retries: 5 ================================================ FILE: docs/add_model_en.md ================================================ # Adding a New Chat Model This guide explains how to add a new chat model to the system. ## Prerequisites - Admin access to the system - API credentials for the model you want to add - Model's API endpoint URL ## Steps to Add a Model ### 1. Access the Admin Interface 1. Log in as an admin user 2. Navigate to the Admin section 3. Go to the "Models" tab image ### 2. Fill in the Model Details Fill in the following fields in the Add Model form: - **Name**: Internal name for the model (e.g. "gpt-3.5-turbo") - **Label**: Display name for the model (e.g. "GPT-3.5 Turbo") - **URL**: API endpoint URL for the model - **API Auth Header**: Header name for authentication (e.g. "Authorization", "x-api-key") - **API Auth Key**: Environment variable containing the API key - **Is Default**: Whether this should be the default model - **Enable Per-Mode Rate Limit**: Enable rate limiting for this specific model - **Order Number**: Position in the model list (lower numbers appear first) - **Default Tokens**: Default token limit for requests - **Max Tokens**: Maximum token limit for requests image ### 3. Add the Model Click "Confirm" to add the model. The system will: 1. Validate the input 2. Create the model record in the database 3. Make the model available for use ### 4. (Optional) Set Rate Limits If you enabled per-mode rate limiting: 1. Go to the "Rate Limits" tab 2. Set rate limits for specific users ## Example Configurations Here are example JSON configurations you can paste into the form: ```json # openai { "name": "gpt-4", "label": "GPT-4", "url": "https://api.openai.com/v1/chat/completions", "apiAuthHeader": "Authorization", "apiAuthKey": "OPENAI_API_KEY", "isDefault": false, "enablePerModeRatelimit": true, "orderNumber": 5, "defaultToken": 4096, "maxToken": 8192 } # claude { "name": "claude-3-7-sonnet-20250219", "label": "claude-3-7-sonnet-20250219", "url": "https://api.anthropic.com/v1/messages", "apiAuthHeader": "x-api-key", "apiAuthKey": "CLAUDE_API_KEY", "isDefault": false, "enablePerModeRatelimit": false, "isEnable": true, "orderNumber": 0, "defaultToken": 4096, "maxToken": 4096 } # gemini { "name": "gemini-2.0-flash", "label": "gemini-2.0-flash", "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash", "apiAuthHeader": "GEMINI_API_KEY", "apiAuthKey": "GEMINI_API_KEY", "isDefault": true, "enablePerModeRatelimit": false, "isEnable": true, "orderNumber": 0, "defaultToken": 4096, "maxToken": 4096 } # deepseek { "name": "deepseek-chat", "label": "deepseek-chat", "url": "https://api.deepseek.com/v1/chat/completions", "apiAuthHeader": "Authorization", "apiAuthKey": "DEEPSEEK_API_KEY", "isDefault": false, "enablePerModeRatelimit": false, "isEnable": true, "orderNumber": 0, "defaultToken": 8192, "maxToken": 8192 } # open router { "name": "deepseek/deepseek-r1:free", "label": "deepseek/deepseek-r1(OR)", "url": "https://openrouter.ai/api/v1/chat/completions", "apiAuthHeader": "Authorization", "apiAuthKey": "OPENROUTER_API_KEY", "isDefault": false, "enablePerModeRatelimit": false, "isEnable": true, "orderNumber": 1, "defaultToken": 8192, "maxToken": 8192 } ``` ## Troubleshooting **Model not appearing?** - Check if the model was added successfully in the database - Verify the API credentials are correct - Ensure the API endpoint is accessible **Rate limiting issues?** - Verify rate limits are properly configured - Check user permissions - Review system logs for errors ================================================ FILE: docs/add_model_zh.md ================================================ # 添加新聊天模型 本指南介绍如何向系统添加新的聊天模型。 ## 先决条件 - 系统管理员权限 - 要添加模型的API凭证 - 模型的API端点URL ## 添加模型的步骤 ### 1. 访问管理员界面 1. 以管理员用户登录 2. 导航到管理员部分 3. 进入"模型"标签页 image ### 2. 填写模型详情 在添加模型表单中填写以下字段: - **名称**: 模型的内部名称 (如 "gpt-3.5-turbo") - **标签**: 模型的显示名称 (如 "GPT-3.5 Turbo") - **URL**: 模型的API端点URL - **API认证头**: 认证头名称 (如 "Authorization", "x-api-key") - **API认证密钥**: 包含API密钥的环境变量 - **是否默认**: 是否设为默认模型 - **启用模式限速**: 为此特定模型启用速率限制 - **排序号**: 在模型列表中的位置(数字越小越靠前) - **默认token数**: 请求的默认token限制 - **最大token数**: 请求的最大token限制 image ### 3. 添加模型 点击"确认"添加模型。系统将: 1. 验证输入 2. 在数据库中创建模型记录 3. 使模型可供使用 ### 4. (可选)设置速率限制 如果启用了模式限速: 1. 进入"速率限制"标签页 2. 为特定用户设置速率限制 ## 示例配置 以下是可粘贴到表单中的示例JSON配置: ```json # openai { "name": "gpt-4", "label": "GPT-4", "url": "https://api.openai.com/v1/chat/completions", "apiAuthHeader": "Authorization", "apiAuthKey": "OPENAI_API_KEY", "isDefault": false, "enablePerModeRatelimit": true, "orderNumber": 5, "defaultToken": 4096, "maxToken": 8192 } # claude { "name": "claude-3-7-sonnet-20250219", "label": "claude-3-7-sonnet-20250219", "url": "https://api.anthropic.com/v1/messages", "apiAuthHeader": "x-api-key", "apiAuthKey": "CLAUDE_API_KEY", "isDefault": false, "enablePerModeRatelimit": false, "isEnable": true, "orderNumber": 0, "defaultToken": 4096, "maxToken": 4096 } # gemini { "name": "gemini-2.0-flash", "label": "gemini-2.0-flash", "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash", "apiAuthHeader": "GEMINI_API_KEY", "apiAuthKey": "GEMINI_API_KEY", "isDefault": true, "enablePerModeRatelimit": false, "isEnable": true, "orderNumber": 0, "defaultToken": 4096, "maxToken": 4096 } # deepseek { "name": "deepseek-chat", "label": "deepseek-chat", "url": "https://api.deepseek.com/v1/chat/completions", "apiAuthHeader": "Authorization", "apiAuthKey": "DEEPSEEK_API_KEY", "isDefault": false, "enablePerModeRatelimit": false, "isEnable": true, "orderNumber": 0, "defaultToken": 8192, "maxToken": 8192 } # open router { "name": "deepseek/deepseek-r1:free", "label": "deepseek/deepseek-r1(OR)", "url": "https://openrouter.ai/api/v1/chat/completions", "apiAuthHeader": "Authorization", "apiAuthKey": "OPENROUTER_API_KEY", "isDefault": false, "enablePerModeRatelimit": false, "isEnable": true, "orderNumber": 1, "defaultToken": 8192, "maxToken": 8192 } # silicon flow { "name": "Qwen/Qwen3-235B-A22B", "label": "Qwen/Qwen3-235B-A22B", "url": "https://api.siliconflow.cn/v1/chat/completions", "apiAuthHeader": "Authorization", "apiAuthKey": "SILICONFLOW_API_KEY", "isDefault": false, "enablePerModeRatelimit": false, "isEnable": true, "orderNumber": 0, "defaultToken": 0, "maxToken": 0 } ``` ## 故障排除 **模型未出现?** - 检查模型是否成功添加到数据库 - 验证API凭证是否正确 - 确保API端点可访问 **速率限制问题?** - 验证速率限制是否正确配置 - 检查用户权限 - 查看系统日志中的错误 ================================================ FILE: docs/artifact_gallery_en.md ================================================ # Artifact Gallery ## Overview The Artifact Gallery is a comprehensive management interface for viewing, organizing, and interacting with code artifacts generated during chat conversations. It provides a centralized location to browse, execute, and manage all artifacts created across different chat sessions. ## Features ### 1. **Artifact Management** - **Browse artifacts** from all chat sessions in one place - **Filter and search** by type, language, session, or content - **Sort artifacts** by creation date, type, execution count, or rating - **View statistics** about artifact usage and performance - **Export artifacts** to JSON format for backup or sharing ### 2. **Code Execution** - **Run JavaScript/Python code** directly in the gallery - **Real-time output** with syntax highlighting - **Error handling** with detailed error messages - **Performance metrics** including execution time - **Library support** for popular packages (lodash, d3, numpy, pandas, etc.) ### 3. **Artifact Viewing** - **HTML rendering** in secure iframe sandbox - **SVG visualization** with proper scaling - **JSON formatting** with validation - **Mermaid diagram** rendering - **Syntax highlighting** for code artifacts ### 4. **Organization Tools** - **Grid and list views** for different browsing preferences - **Pagination** for large artifact collections - **Tag-based categorization** with automatic tag generation - **Session context** showing which chat created each artifact - **Duplicate detection** and management ## Accessing the Gallery ### From Chat Interface 1. Click the **Gallery** button (gallery icon) in the chat footer 2. The gallery will open in place of the chat interface 3. Use the same gallery button to return to chat view ### Navigation - **Toggle between chat and gallery** using the gallery button - **Switch between grid and list views** using the view toggle - **Use filters** to narrow down artifacts by specific criteria ## Artifact Types ### Executable Artifacts These artifacts can be run directly in the gallery: #### JavaScript/TypeScript - **Supported features**: ES6+, async/await, classes, modules - **Available libraries**: lodash, d3, chart.js, moment, axios, rxjs, p5, three, fabric - **Canvas support**: Create interactive graphics and visualizations - **Execution environment**: Secure Web Worker sandbox #### Python - **Supported features**: Python 3.x syntax, scientific computing - **Available packages**: numpy, pandas, matplotlib, scipy, scikit-learn, requests, beautifulsoup4, pillow, sympy, networkx, seaborn, plotly, bokeh, altair - **Plot support**: Matplotlib plots rendered as images - **Execution environment**: Pyodide-based Python interpreter ### Viewable Artifacts These artifacts are rendered for visual inspection: #### HTML - **Secure rendering**: Iframe sandbox with restricted permissions - **Full HTML support**: CSS, basic JavaScript, forms, modals - **Responsive design**: Adapts to different screen sizes - **External links**: Open in new window capability #### SVG - **Vector graphics**: Scalable and crisp rendering - **Interactive elements**: Hover effects and basic interactions - **Theme adaptation**: Adjusts colors for dark/light themes - **Export capability**: Download as SVG files #### JSON - **Formatted display**: Pretty-printed with syntax highlighting - **Validation**: Automatic JSON syntax validation - **Copy functionality**: Easy copying of formatted JSON - **Large file support**: Handles large JSON structures efficiently #### Mermaid - **Diagram types**: Flowcharts, sequence diagrams, class diagrams, etc. - **Auto-rendering**: Automatic conversion to visual diagrams - **Responsive**: Adapts to container size - **Theme support**: Follows application theme ## Interface Elements ### Gallery Header - **Title and count**: Shows total number of artifacts - **Action buttons**: Access to filters, statistics, and export - **Search bar**: Quick text search across all artifacts ### Filter Panel - **Search**: Text search across titles, content, and tags - **Type filter**: Filter by artifact type (code, HTML, SVG, etc.) - **Language filter**: Filter by programming language - **Session filter**: Filter by chat session - **Date range**: Filter by creation date - **Sort options**: Multiple sorting criteria ### Statistics Panel - **Total artifacts**: Overall count of artifacts - **Execution stats**: Total runs, average execution time, success rate - **Type breakdown**: Distribution of artifact types - **Language distribution**: Popular programming languages - **Performance charts**: Visual representation of usage patterns ### Artifact Cards (Grid View) - **Artifact preview**: Truncated code or content preview - **Metadata**: Creation date, language, session info - **Action buttons**: Preview, Run/View, Edit, Duplicate, Delete - **Tags**: Automatically generated and custom tags - **Execution count**: Number of times the artifact has been run ### Artifact Rows (List View) - **Compact layout**: More artifacts visible at once - **Essential info**: Title, type, language, creation date - **Quick actions**: All management functions accessible - **Session context**: Clear indication of source chat session ## Actions and Operations ### Running Code Artifacts 1. **Click the Run button** (play icon) on JavaScript/Python artifacts 2. **Run modal opens** with code preview and execution controls 3. **Execute code** using the "Run Code" button 4. **View output** in real-time with syntax highlighting 5. **Clear results** to run again with fresh environment ### Viewing Visual Artifacts 1. **Click the View button** (external link icon) on HTML/SVG/JSON artifacts 2. **View modal opens** with proper rendering 3. **Interact with content** (for HTML artifacts) 4. **Copy content** using the copy button 5. **Close modal** to return to gallery ### Managing Artifacts - **Preview**: Quick view of artifact content - **Edit**: Modify artifact content and save changes - **Duplicate**: Create a copy for experimentation - **Delete**: Remove artifact permanently (with confirmation) - **Copy**: Copy artifact content to clipboard - **Download**: Save artifact as file with proper extension ## Advanced Features ### Automatic Tagging The gallery automatically generates tags based on: - **Programming language** (javascript, python, html, etc.) - **Framework usage** (react, vue, pandas, matplotlib, etc.) - **Code patterns** (async, functions, classes, loops, etc.) - **Libraries** (lodash, d3, numpy, etc.) - **Execution results** (error, success, slow, fast, etc.) ### Session Context Each artifact maintains connection to its source: - **Session title**: Name of the chat session - **Message context**: Which message created the artifact - **Timestamp**: When the artifact was created - **Edit tracking**: Whether the artifact has been modified ### Performance Tracking - **Execution time**: How long code takes to run - **Success rate**: Percentage of successful executions - **Error patterns**: Common error types and frequencies - **Usage statistics**: Most frequently run artifacts ### Export and Backup - **JSON export**: Complete artifact data with metadata - **Filtered export**: Export only selected or filtered artifacts - **Backup format**: Structured for easy import/restore - **Sharing**: Share artifact collections with others ## Best Practices ### Organization - **Use descriptive titles** for artifacts when creating them - **Add custom tags** for better organization - **Regular cleanup** of outdated or experimental artifacts - **Session naming** to provide better context ### Code Execution - **Test incrementally** when developing complex code - **Use console.log** for debugging JavaScript - **Handle errors gracefully** in your code - **Be mindful of execution time** for complex operations ### Performance - **Use pagination** for large artifact collections - **Clear execution results** when not needed - **Filter artifacts** to focus on relevant items - **Regular exports** for backup and archival ## Security Considerations ### Code Execution - **Sandboxed environment**: All code runs in isolated Web Workers - **No network access**: Direct network requests are blocked - **No file system access**: Cannot read/write local files - **Limited DOM access**: Cannot modify the parent application - **Timeout protection**: Long-running code is automatically terminated ### Content Rendering - **HTML sandboxing**: Iframe sandbox prevents malicious scripts - **SVG sanitization**: Removes potentially dangerous elements - **Content validation**: JSON and other formats are validated - **Safe origins**: External resources are restricted ## Troubleshooting ### Common Issues - **Code not running**: Check language support and syntax - **Artifacts not loading**: Refresh the page or check browser console - **Performance issues**: Reduce artifact count or use filters - **Export failures**: Check browser download permissions ### Browser Compatibility - **Modern browsers**: Chrome, Firefox, Safari, Edge (latest versions) - **JavaScript required**: Gallery requires JavaScript to function - **LocalStorage**: Used for preferences and temporary data - **WebWorkers**: Required for code execution features ## API Integration The gallery integrates with the chat application's API: ```javascript // Example: Getting artifacts from a specific session const messages = await fetch(`/api/uuid/chat_messages/chat_sessions/${sessionId}`); // Artifacts are included in each message payload as the `artifacts` field // Example: Executing code artifact // Code execution is handled client-side in Artifact Editor/Sandbox components, // not by a dedicated backend `/api/artifacts/:id/execute` endpoint. ``` ## Future Enhancements ### Planned Features - **Artifact versioning**: Track changes and maintain history - **Collaborative editing**: Multiple users editing artifacts - **Advanced analytics**: Detailed usage and performance metrics - **Template system**: Create reusable artifact templates - **AI suggestions**: Automatic code improvements and suggestions ### Community Features - **Artifact sharing**: Public gallery for sharing useful artifacts - **Rating system**: Community ratings for popular artifacts - **Comments**: Collaborative discussion on artifacts - **Collections**: Curated sets of related artifacts This documentation provides a comprehensive guide to using the Artifact Gallery effectively. For technical implementation details, see the source code in `/src/views/chat/components/ArtifactGallery.vue`. ================================================ FILE: docs/artifact_gallery_zh.md ================================================ # 制品画廊 ## 概述 制品画廊是一个综合管理界面,用于查看、组织和与聊天对话中生成的代码制品进行交互。它提供了一个集中的位置来浏览、执行和管理不同聊天会话中创建的所有制品。 ## 功能特性 ### 1. **制品管理** - **浏览制品** 在一个地方查看所有聊天会话的制品 - **过滤和搜索** 按类型、语言、会话或内容进行筛选 - **排序制品** 按创建日期、类型、执行次数或评分排序 - **查看统计** 关于制品使用和性能的统计信息 - **导出制品** 以JSON格式导出以供备份或分享 ### 2. **代码执行** - **运行JavaScript/Python代码** 直接在画廊中执行 - **实时输出** 带语法高亮的输出 - **错误处理** 详细的错误消息 - **性能指标** 包括执行时间 - **库支持** 支持流行的包(lodash, d3, numpy, pandas等) ### 3. **制品查看** - **HTML渲染** 在安全的iframe沙盒中 - **SVG可视化** 带正确缩放 - **JSON格式化** 带验证功能 - **Mermaid图表** 渲染 - **语法高亮** 用于代码制品 ### 4. **组织工具** - **网格和列表视图** 不同的浏览偏好 - **分页** 用于大型制品集合 - **基于标签的分类** 自动标签生成 - **会话上下文** 显示哪个聊天创建了每个制品 - **重复检测** 和管理 ## 访问画廊 ### 从聊天界面 1. 点击聊天页脚的**画廊**按钮(画廊图标) 2. 画廊将在聊天界面的位置打开 3. 使用相同的画廊按钮返回聊天视图 ### 导航 - **在聊天和画廊之间切换** 使用画廊按钮 - **在网格和列表视图之间切换** 使用视图切换 - **使用过滤器** 按特定条件缩小制品范围 ## 制品类型 ### 可执行制品 这些制品可以直接在画廊中运行: #### JavaScript/TypeScript - **支持的功能**:ES6+、async/await、类、模块 - **可用库**:lodash、d3、chart.js、moment、axios、rxjs、p5、three、fabric - **Canvas支持**:创建交互式图形和可视化 - **执行环境**:安全的Web Worker沙盒 #### Python - **支持的功能**:Python 3.x语法、科学计算 - **可用包**:numpy、pandas、matplotlib、scipy、scikit-learn、requests、beautifulsoup4、pillow、sympy、networkx、seaborn、plotly、bokeh、altair - **绘图支持**:Matplotlib图表渲染为图像 - **执行环境**:基于Pyodide的Python解释器 ### 可查看制品 这些制品用于视觉检查: #### HTML - **安全渲染**:带限制权限的iframe沙盒 - **完整HTML支持**:CSS、基本JavaScript、表单、模态框 - **响应式设计**:适应不同屏幕尺寸 - **外部链接**:在新窗口中打开的能力 #### SVG - **矢量图形**:可缩放和清晰的渲染 - **交互元素**:悬停效果和基本交互 - **主题适应**:为深色/浅色主题调整颜色 - **导出能力**:下载为SVG文件 #### JSON - **格式化显示**:带语法高亮的美化打印 - **验证**:自动JSON语法验证 - **复制功能**:轻松复制格式化的JSON - **大文件支持**:高效处理大型JSON结构 #### Mermaid - **图表类型**:流程图、序列图、类图等 - **自动渲染**:自动转换为视觉图表 - **响应式**:适应容器大小 - **主题支持**:遵循应用主题 ## 界面元素 ### 画廊标题 - **标题和计数**:显示制品总数 - **操作按钮**:访问过滤器、统计和导出 - **搜索栏**:跨所有制品的快速文本搜索 ### 过滤器面板 - **搜索**:跨标题、内容和标签的文本搜索 - **类型过滤**:按制品类型过滤(代码、HTML、SVG等) - **语言过滤**:按编程语言过滤 - **会话过滤**:按聊天会话过滤 - **日期范围**:按创建日期过滤 - **排序选项**:多个排序条件 ### 统计面板 - **总制品**:制品总数 - **执行统计**:总运行次数、平均执行时间、成功率 - **类型分布**:制品类型的分布 - **语言分布**:流行的编程语言 - **性能图表**:使用模式的视觉表示 ### 制品卡片(网格视图) - **制品预览**:截断的代码或内容预览 - **元数据**:创建日期、语言、会话信息 - **操作按钮**:预览、运行/查看、编辑、复制、删除 - **标签**:自动生成和自定义标签 - **执行计数**:制品被运行的次数 ### 制品行(列表视图) - **紧凑布局**:一次显示更多制品 - **基本信息**:标题、类型、语言、创建日期 - **快速操作**:所有管理功能都可访问 - **会话上下文**:源聊天会话的清楚指示 ## 操作和功能 ### 运行代码制品 1. **点击运行按钮**(播放图标)在JavaScript/Python制品上 2. **运行模态框打开** 带代码预览和执行控件 3. **执行代码** 使用"运行代码"按钮 4. **查看输出** 实时显示带语法高亮的输出 5. **清除结果** 在新环境中重新运行 ### 查看视觉制品 1. **点击查看按钮**(外部链接图标)在HTML/SVG/JSON制品上 2. **查看模态框打开** 带正确渲染 3. **与内容交互**(对于HTML制品) 4. **复制内容** 使用复制按钮 5. **关闭模态框** 返回画廊 ### 管理制品 - **预览**:快速查看制品内容 - **编辑**:修改制品内容并保存更改 - **复制**:创建副本用于实验 - **删除**:永久删除制品(带确认) - **复制**:将制品内容复制到剪贴板 - **下载**:将制品保存为带正确扩展名的文件 ## 高级功能 ### 自动标签 画廊基于以下内容自动生成标签: - **编程语言**(javascript、python、html等) - **框架使用**(react、vue、pandas、matplotlib等) - **代码模式**(async、functions、classes、loops等) - **库**(lodash、d3、numpy等) - **执行结果**(error、success、slow、fast等) ### 会话上下文 每个制品维护与其源的连接: - **会话标题**:聊天会话的名称 - **消息上下文**:哪个消息创建了制品 - **时间戳**:制品创建的时间 - **编辑跟踪**:制品是否已被修改 ### 性能跟踪 - **执行时间**:代码运行所需的时间 - **成功率**:成功执行的百分比 - **错误模式**:常见错误类型和频率 - **使用统计**:最常运行的制品 ### 导出和备份 - **JSON导出**:带元数据的完整制品数据 - **过滤导出**:仅导出选定或过滤的制品 - **备份格式**:结构化以便于导入/恢复 - **分享**:与他人分享制品集合 ## 最佳实践 ### 组织 - **使用描述性标题** 创建制品时 - **添加自定义标签** 更好的组织 - **定期清理** 过时或实验性制品 - **会话命名** 提供更好的上下文 ### 代码执行 - **增量测试** 开发复杂代码时 - **使用console.log** 调试JavaScript - **优雅处理错误** 在您的代码中 - **注意执行时间** 对于复杂操作 ### 性能 - **使用分页** 用于大型制品集合 - **清除执行结果** 当不需要时 - **过滤制品** 专注于相关项目 - **定期导出** 用于备份和存档 ## 安全考虑 ### 代码执行 - **沙盒环境**:所有代码在隔离的Web Worker中运行 - **无网络访问**:直接网络请求被阻止 - **无文件系统访问**:无法读写本地文件 - **有限DOM访问**:无法修改父应用程序 - **超时保护**:长时间运行的代码自动终止 ### 内容渲染 - **HTML沙盒**:iframe沙盒防止恶意脚本 - **SVG清理**:删除潜在危险元素 - **内容验证**:JSON和其他格式被验证 - **安全来源**:外部资源受限 ## 故障排除 ### 常见问题 - **代码不运行**:检查语言支持和语法 - **制品不加载**:刷新页面或检查浏览器控制台 - **性能问题**:减少制品数量或使用过滤器 - **导出失败**:检查浏览器下载权限 ### 浏览器兼容性 - **现代浏览器**:Chrome、Firefox、Safari、Edge(最新版本) - **需要JavaScript**:画廊需要JavaScript才能运行 - **本地存储**:用于偏好设置和临时数据 - **WebWorkers**:代码执行功能所需 ## API集成 画廊与聊天应用程序的API集成: ```javascript // 示例:从特定会话获取制品 const messages = await fetch(`/api/uuid/chat_messages/chat_sessions/${sessionId}`); // 制品包含在每条消息的 `artifacts` 字段中 // 示例:执行代码制品 // 代码执行由前端的 Artifact Editor/Sandbox 组件处理, // 后端没有独立的 `/api/artifacts/:id/execute` 接口。 ``` ## 未来增强 ### 计划功能 - **制品版本控制**:跟踪更改并维护历史记录 - **协作编辑**:多用户编辑制品 - **高级分析**:详细的使用和性能指标 - **模板系统**:创建可重用的制品模板 - **AI建议**:自动代码改进和建议 ### 社区功能 - **制品分享**:用于分享有用制品的公共画廊 - **评分系统**:社区对流行制品的评分 - **评论**:制品的协作讨论 - **集合**:相关制品的策划集合 此文档提供了有效使用制品画廊的全面指南。有关技术实现细节,请参阅`/src/views/chat/components/ArtifactGallery.vue`中的源代码。 ================================================ FILE: docs/code_runner_artifacts_tutorial.md ================================================ # Code Runner and Artifacts Tutorial This tutorial shows how to create artifacts in chat messages, run executable code, and use the artifact gallery. It is written for end users and references the behavior implemented in the current frontend and backend. ## What Are Artifacts? Artifacts are structured blocks inside a chat response that the app can detect and render specially. Supported artifact types include: - Code (plain or executable) - HTML - SVG - Mermaid diagrams - JSON - Markdown The backend extracts artifacts from assistant messages and stores them with the chat message. The frontend renders them in the message view and in the Artifact Gallery. ## Create Artifacts in Chat Artifacts are detected when the assistant responds with fenced code blocks that include a marker comment. Use these exact formats: ### Code Artifact (non-executable) ````javascript // Example: a plain code artifact ```javascript export function clamp(value, min, max) { return Math.min(Math.max(value, min), max) } ``` ```` ### Executable Code Artifact ````javascript ```javascript console.log('Hello from the code runner') ``` ```` ### HTML Artifact ````html ```html

Hello HTML

This renders inside a sandboxed iframe.

``` ```` ### SVG Artifact ````svg ```svg ``` ```` ### Mermaid Artifact ````mermaid ```mermaid graph TD A[Start] --> B{Decision} B -->|Yes| C[Proceed] B -->|No| D[Stop] ``` ```` ### JSON Artifact ````json ```json { "status": "ok", "items": [1, 2, 3] } ``` ```` ## Run Executable Code Executable artifacts show a Run button in the message view and in the Artifact Gallery. Supported languages: - JavaScript, TypeScript - Python (via Pyodide) To run a snippet: 1. Create an executable code artifact using the `` marker. 2. Open the artifact in the message view or the gallery. 3. Click Run. Execution output is shown in the artifact panel and includes logs, returns, and errors. ## Load JavaScript Libraries The JavaScript runner can load a small set of libraries from CDNs. Use this import comment at the top of your code: ```javascript // @import lodash console.log(_.chunk([1, 2, 3, 4], 2)) ``` Available JS libraries: - lodash - d3 - chart.js - moment - axios - rxjs - p5 - three - fabric ## Load Python Packages The Python runner auto-detects common imports and loads supported packages. ```python import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x) plt.plot(x, y) ``` Available Python packages include: - numpy, pandas, matplotlib, scipy, scikit-learn - requests, beautifulsoup4, pillow, sympy, networkx - seaborn, plotly, bokeh, altair ## Use the Virtual File System (VFS) Both runners provide a simple in-memory file system. Use absolute paths to target the VFS. ### JavaScript Example ```javascript const fs = require('fs') fs.writeFileSync('/workspace/hello.txt', 'Hello VFS') console.log(fs.readFileSync('/workspace/hello.txt', 'utf8')) ``` ### Python Example ```python with open('/workspace/hello.txt', 'w') as f: f.write('Hello VFS') with open('/workspace/hello.txt', 'r') as f: print(f.read()) ``` Default directories: - `/workspace` - `/data` - `/tmp` ## Artifact Gallery The Artifact Gallery aggregates all artifacts from your chat sessions in one place. You can: - Search, filter, and sort artifacts - Preview and edit artifacts - Run executable artifacts - Export artifacts to JSON Open the gallery from the chat UI footer (desktop). ## Tips - Use clear artifact titles so they are easy to find later. - Keep executable snippets short and focused. - Prefer JSON artifacts for structured outputs you want to view or copy. - Use HTML artifacts for small interactive demos (no external dependencies). ================================================ FILE: docs/code_runner_capabilities.md ================================================ # Code Runner Capabilities This document summarizes what the current chat + code runner can do today. ## Execution - JavaScript / TypeScript execution in a sandboxed worker - Python execution in Pyodide - Console output, return values, and error reporting - Timeouts and basic resource limits ## Libraries and Packages ### JavaScript libraries (via `// @import`) - lodash - d3 - chart.js - moment - axios - rxjs - p5 - three - fabric ### Python packages (auto‑loaded in Pyodide) - numpy, pandas, matplotlib, scipy, scikit‑learn - requests, beautifulsoup4, pillow, sympy, networkx - seaborn, plotly, bokeh, altair ## Artifacts - Code artifacts and executable code artifacts - HTML artifacts (rendered in a sandboxed iframe) - SVG, Mermaid, JSON, and Markdown artifacts - Artifact Gallery for browsing, editing, and running artifacts ## Tool Use (Code Runner enabled per session) When **Code Runner** is enabled in session settings, the model can use tools: - `run_code` — Execute JS/TS or Python - `read_vfs` — Read a file from the VFS - `write_vfs` — Write a file to the VFS - `list_vfs` — List directory entries - `stat_vfs` — File/directory metadata Tool results are returned automatically, and the model continues with normal responses or artifacts. ## Virtual File System (VFS) - In‑memory filesystem with `/data`, `/workspace`, `/tmp` - File upload via the UI - Read/write from code (both JS and Python) - VFS state syncs to runners before execution ## Typical Workflows - Load CSVs and analyze with pandas - Generate plots with matplotlib - Prototype JS utilities or data transforms - Build small HTML/SVG demos in artifacts ## Known Constraints - No direct DOM access for JS - No direct network requests from user code - VFS only (no access to host filesystem) ================================================ FILE: docs/code_runner_csv_tutorial.md ================================================ # Tutorial: Chat + File Upload + Python Artifact (Iris CSV) This walkthrough shows how to create a chat with a system prompt, upload `iris.csv`, and ask the model to generate a Python executable artifact that reads the CSV from the VFS and prints results. ## 1) Create a New Chat Session 1. Open the app and click **New Chat**. 2. Set a clear title, for example: `Iris CSV Analysis`. 3. Open the session settings and add a **System Message** like this: ```text You are a data assistant. Always answer with a Python executable artifact when asked to analyze data files. Use VFS paths like /workspace or /data. Print a small table and summary stats. ``` Why this works: - The artifact extractor looks for fenced code blocks with ``. - The Python runner reads files from the in-memory VFS at `/workspace`, `/data`, or `/tmp`. ## 2) Upload the CSV File to the VFS 1. In the chat view, find the **VFS upload** area (often labeled “Upload files for code runners”). 2. Upload your `iris.csv` file. 3. Note the upload location: - Most uploads land under `/data/` or `/workspace/` in the VFS. 4. For this tutorial, we will assume the file is available at: ```text /data/iris.csv ``` If your UI shows a different path, use that path in the next step. ## 3) Prompt the LLM to Generate a Python Artifact Send a user message like this: ```text Use Python to load the CSV at /data/iris.csv, show the first 5 rows, and print summary stats. Return your answer as an executable artifact. ``` Expected artifact structure (example): ````python ```python import pandas as pd df = pd.read_csv('/data/iris.csv') print("Head:") print(df.head()) print("\nSummary:") print(df.describe()) ``` ```` ## 4) Run the Artifact 1. The message will show an artifact panel. 2. Click **Run**. 3. Review output in the execution panel: - `stdout` for printed tables - `return` if a value is returned - `error` if the run fails ## Troubleshooting - **File not found**: Confirm the path from the upload UI and update the code. - **Missing package**: Use supported packages (pandas, numpy, matplotlib, etc.). The runner auto-loads common imports. - **No output**: Ensure the code uses `print()` or returns a value. ================================================ FILE: docs/custom_model_api_en.md ================================================ # Custom Model API (Custom Provider) This document describes the custom model API contract used by the backend when a chat model is configured with `api_type = custom`. ## When this path is used The backend selects the custom provider when the chat model's `api_type` is set to `custom` (Admin → Models → Add Model). ## Required model config (Admin UI) - **API Type**: `Custom` - **URL**: Your custom API endpoint (HTTP POST). - **API Auth Header**: The header name to pass the API key (optional). - **API Auth Key**: The *environment variable name* that stores the API key. Example: - API Auth Header: `x-api-key` - API Auth Key: `MY_CUSTOM_API_KEY` - Then set `MY_CUSTOM_API_KEY=...` in the backend environment. ## Request format The backend always sends a JSON POST body with the following fields: ```json { "prompt": "\n\nHuman: Hello\n\nAssistant: Hi there!\n\nHuman: ...\n\nAssistant: ", "model": "custom-my-model", "max_tokens_to_sample": 2048, "temperature": 0.7, "stop_sequences": ["\n\nHuman:"], "stream": true } ``` Notes: - `prompt` is formatted in a Claude-style transcript. Each non-assistant message becomes: `\n\nHuman: \n\nAssistant: ` - `model` is the chat model name stored in the database. - `max_tokens_to_sample` and `temperature` come from the session settings. - `stream` is always `true` for the custom provider. Headers added by the backend: - `Content-Type: application/json` - `Accept: text/event-stream` - `Cache-Control: no-cache` - `Connection: keep-alive` - Optional auth header if configured (e.g. `x-api-key: `). ## Streaming response format (required) Your API must respond with `text/event-stream` (SSE) and emit lines that start with `data: `. Each `data:` line must contain JSON that includes a `completion` field. The backend **expects full text so far** in `completion` on each event (not deltas). Example stream: ``` data: {"completion":"Hello","stop":null,"stop_reason":null,"truncated":false,"log_id":"1","model":"custom-my-model","exception":null} data: {"completion":"Hello there","stop":null,"stop_reason":null,"truncated":false,"log_id":"1","model":"custom-my-model","exception":null} data: [DONE] ``` The stream ends when you send a line starting with `data: [DONE]`. ## Minimum response fields The backend only uses `completion`, but it unmarshals the following fields: ```json { "completion": "string", "stop": "string or null", "stop_reason": "string or null", "truncated": false, "log_id": "string", "model": "string", "exception": {} } ``` ## Quick checklist - Endpoint accepts JSON POST with the fields listed above. - Supports SSE with `data: {json}\n` lines. - Sends full accumulated text in `completion` each event. - Ends with `data: [DONE]`. - Configure the model in Admin with `api_type = custom` and correct auth env var. ================================================ FILE: docs/deployment_en.md ================================================ ## How to Deploy Refer to `docker-compose.yaml` [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/tk7jWU?referralCode=5DMfQv) Then configure the environment variables. ``` PORT=8080 OPENAI_RATELIMIT=0 ``` Fill in the other two keys if you have them. image After deployment, registering users, the first user is an administrator, then go to to set rate limiting. Public deployment, only adds rate limiting to trusted emails, so even if someone registers, it will not be available. image This helps ensure only authorized users can access the deployed system by limiting registration to trusted emails and enabling rate limiting controls. ================================================ FILE: docs/deployment_zh.md ================================================ ## 如何部署 参考 `docker-compose.yaml` [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/tk7jWU?referralCode=5DMfQv) 然后配置环境变量就可以了. ``` PORT=8080 OPENAI_RATELIMIT=0 ``` 别的两个 api key 有就填. image 部署之后, 注册用户, 第一个用户是管理员, 然后到 , 设置 ratelimit, 公网部署, 只对信任的email 增加 ratelimit, 这样即使有人注册, 也是不能用的. image ================================================ FILE: docs/dev/ERROR_HANDLING_STANDARDS.md ================================================ # Error Handling Standardization Guide ## Current Issues - Inconsistent use of `http.Error()` vs `RespondWithAPIError()` - Mixed error response formats - Some areas missing proper logging ## Standards to Follow ### 1. Use RespondWithAPIError for all API responses Replace: ```go http.Error(w, err.Error(), http.StatusBadRequest) ``` With: ```go RespondWithAPIError(w, ErrValidationInvalidInput("Failed to decode request body").WithDebugInfo(err.Error())) ``` ### 2. Always log errors before responding ```go log.WithError(err).Error("Context-specific error message") RespondWithAPIError(w, ErrInternalUnexpected.WithMessage("User-friendly message")) ``` ### 3. Use appropriate error types - `ErrValidationInvalidInput` for bad request data - `ErrResourceNotFound` for 404 errors - `ErrInternalUnexpected` for 500 errors - `ErrPermissionDenied` for 403 errors ### 4. Files that need standardization - chat_auth_user_handler.go (partially fixed) - admin_handler.go - chat_prompt_hander.go - chat_comment_handler.go - chat_message_handler.go - handle_tts.go ## Implementation Priority 1. Authentication handlers (high impact) 2. Core chat functionality 3. Admin and utility handlers This standardization should be done gradually to avoid breaking changes. ================================================ FILE: docs/dev/INTEGRATION_GUIDE.md ================================================ # 🚀 Quick Integration Guide: Add File Upload to Chat This guide shows you exactly how to add VFS file upload to your chat session in 3 simple steps. ## 📂 Files to Add First, make sure you have these new components in your project: ``` web/src/components/ ├── VFSProvider.vue ✅ (already created) ├── ChatVFSUploader.vue ✅ (already created) ├── VFSFileManager.vue ✅ (already created) └── VFSIntegration.vue ✅ (already created) web/src/utils/ ├── virtualFileSystem.js ✅ (already created) └── vfsImportExport.js ✅ (already created) ``` ## 🔧 Step 1: Modify Conversation.vue Add these **3 lines** to your `web/src/views/chat/components/Conversation.vue`: ### Add Imports (at the top with other imports): ```typescript // Add these two lines to existing imports import ChatVFSUploader from '@/components/ChatVFSUploader.vue' import VFSProvider from '@/components/VFSProvider.vue' ``` ### Add Event Handler (in script setup section): ```typescript // Add this event handler with your other functions const handleVFSFileUploaded = (fileInfo: any) => { nui_msg.success(`📁 File uploaded: ${fileInfo.filename}`) // Optional: Add a helpful message to chat const helpMessage = `File uploaded to VFS! Use this code to access it: **Python:** \`\`\`python # For ${fileInfo.filename} import pandas as pd # if CSV data = pd.read_csv('${fileInfo.path}') print(data.head()) \`\`\` **JavaScript:** \`\`\`javascript // For ${fileInfo.filename} const fs = require('fs'); const content = fs.readFileSync('${fileInfo.path}', 'utf8'); console.log(content); \`\`\`` addChat( sessionUuid, { uuid: uuidv7(), dateTime: nowISO(), text: helpMessage, inversion: false, error: false, loading: false, }, ) } ``` ### Modify Template (wrap and add uploader): Find your existing template and make these changes: **BEFORE:** ```vue ``` **AFTER:** ```vue ``` ## 🎯 Step 2: Test It! 1. **Start your dev server** 2. **Go to any chat session** 3. **Look for the folder icon button** in the bottom right of the chat 4. **Click it and upload a CSV or JSON file** 5. **The system will show code examples** for accessing the file 6. **Copy and run the code** in a code runner! ## 📋 Complete Example Here's what a complete integration looks like in your Conversation.vue: ```vue ``` ## 🎉 That's It! You now have: ✅ **File Upload Button** - Users can upload files to VFS ✅ **Auto Code Generation** - System shows how to use uploaded files ✅ **Cross-Language Support** - Files work in both Python and JavaScript ✅ **Session Integration** - Files persist throughout the chat session ✅ **File Manager** - Browse and download VFS files ## 💡 Usage Examples After integration, users can: 1. **Upload** `sales.csv` → VFS stores it at `/data/sales.csv` 2. **Get code examples** automatically in chat 3. **Run Python code:** ```python import pandas as pd df = pd.read_csv('/data/sales.csv') df['profit_margin'] = df['profit'] / df['sales'] * 100 df.to_csv('/data/sales_with_margins.csv', index=False) ``` 4. **Run JavaScript code:** ```javascript const fs = require('fs'); const data = fs.readFileSync('/data/sales_with_margins.csv', 'utf8'); console.log('Processed data:', data.split('\n').length, 'rows'); ``` 5. **Download results** via the file manager The VFS creates a complete data processing workflow within your chat interface! 🚀 ================================================ FILE: docs/dev/code_runner_manual.md ================================================ # Code Runner User Manual ## Overview The Code Runner is an interactive JavaScript execution environment built into the chat application. It allows you to write, run, and experiment with JavaScript code directly in chat messages, with real-time output, graphics support, and access to popular libraries. ## Getting Started ### Creating Executable Code Artifacts There are two ways to create executable code artifacts: #### Method 1: Explicit Executable Syntax ```javascript // @import lodash console.log('Hello, World!') const numbers = [1, 2, 3, 4, 5] const sum = _.sum(numbers) console.log('Sum:', sum) return sum ``` #### Method 2: Using the Executable Marker When asking the AI to create executable code, use this format: ``` Can you create an executable JavaScript function that calculates fibonacci numbers? ``` The AI will automatically create artifacts with the `` marker. ## Basic Features ### 1. Console Output ```javascript console.log('This is a log message') console.error('This is an error message') console.warn('This is a warning') console.info('This is info') ``` ### 2. Return Values ```javascript // The return value is automatically displayed const result = Math.PI * 2 return result ``` ### 3. Error Handling ```javascript try { throw new Error('Something went wrong!') } catch (error) { console.error('Caught error:', error.message) } ``` ## Advanced Features ### 1. Library Loading The Code Runner supports 9 popular JavaScript libraries that can be loaded automatically: - **lodash**: Utility functions - **d3**: Data visualization - **chart.js**: Chart creation - **moment**: Date/time manipulation - **axios**: HTTP requests (limited to safe operations) - **rxjs**: Reactive programming - **p5**: Creative coding - **three**: 3D graphics - **fabric**: Canvas manipulation #### Usage: ```javascript // @import lodash // @import d3 // Use lodash const numbers = [1, 2, 3, 4, 5] const doubled = _.map(numbers, n => n * 2) console.log('Doubled:', doubled) // Use d3 const scale = d3.scaleLinear().domain([0, 10]).range([0, 100]) console.log('Scaled value:', scale(5)) return { doubled, scaled: scale(5) } ``` ### 2. Canvas Graphics Create visual outputs using the built-in canvas support: ```javascript // Create a canvas const canvas = createCanvas(400, 300) const ctx = canvas.getContext('2d') // Draw a colorful rectangle ctx.fillStyle = '#FF6B6B' ctx.fillRect(50, 50, 100, 80) // Draw a circle ctx.fillStyle = '#4ECDC4' ctx.beginPath() ctx.arc(200, 150, 40, 0, 2 * Math.PI) ctx.fill() // Add text ctx.fillStyle = '#45B7D1' ctx.fillText('Hello Canvas!', 100, 200) // Return the canvas to display it return canvas ``` ### 3. Data Visualization with D3 ```javascript // @import d3 // Create a simple bar chart const canvas = createCanvas(400, 300) const ctx = canvas.getContext('2d') const data = [10, 20, 30, 40, 50] const scale = d3.scaleLinear().domain([0, 50]).range([0, 200]) data.forEach((value, index) => { const barHeight = scale(value) const x = index * 60 + 50 const y = 250 - barHeight ctx.fillStyle = `hsl(${index * 60}, 70%, 50%)` ctx.fillRect(x, y, 40, barHeight) ctx.fillStyle = '#000' ctx.fillText(value, x + 15, y - 5) }) return canvas ``` ### 4. Mathematical Computations ```javascript // @import lodash // Generate random data const dataset = _.range(100).map(() => Math.random() * 100) // Calculate statistics const stats = { mean: _.mean(dataset), median: _.sortBy(dataset)[Math.floor(dataset.length / 2)], min: _.min(dataset), max: _.max(dataset), sum: _.sum(dataset) } console.log('Dataset Statistics:', stats) // Visualize distribution const canvas = createCanvas(400, 200) const ctx = canvas.getContext('2d') const buckets = _.range(0, 101, 10) const histogram = buckets.map(bucket => dataset.filter(value => value >= bucket && value < bucket + 10).length ) histogram.forEach((count, index) => { const barHeight = count * 5 const x = index * 35 + 20 const y = 180 - barHeight ctx.fillStyle = '#3498db' ctx.fillRect(x, y, 30, barHeight) ctx.fillStyle = '#000' ctx.fillText(count, x + 10, y - 5) }) return { stats, histogram } ``` ## User Interface Guide ### 1. Code Execution Controls - **▶️ Run Code**: Execute the current code - **🗑️ Clear Output**: Remove all output - **✏️ Edit Mode**: Switch between view and edit modes - **📦 Libraries**: View available libraries ### 2. Library Management Click the "Available" button next to "Libraries" to see: - List of all available libraries - Usage instructions - Import syntax examples ### 3. Output Display The output area shows: - **Console logs**: Blue background - **Errors**: Red background with error details - **Return values**: Purple background - **Canvas graphics**: Rendered inline - **Execution stats**: Time, memory usage, operations ### 4. Keyboard Shortcuts - **Ctrl/Cmd + Enter**: Run code while in edit mode - **Escape**: Exit edit mode ## Performance and Limits ### Resource Limits The Code Runner has built-in safety limits: - **Execution Time**: 10 seconds maximum - **Memory Usage**: ~50MB limit - **Operations**: 100,000 operations max (prevents infinite loops) - **Library Loading**: 30 seconds timeout ### Performance Monitoring Each execution shows: - **Execution time**: How long the code took to run - **Memory usage**: Approximate memory consumption - **Operation count**: Number of operations performed Example output: ``` Execution completed in 45ms | ~2.3MB | 1,247 ops ``` ## Error Handling and Debugging ### Common Errors 1. **Library Not Found**: ``` Error: Library 'unknown' is not available ``` Solution: Check available libraries and use correct names 2. **Memory Limit Exceeded**: ``` Error: Memory limit exceeded: ~52MB ``` Solution: Optimize code to use less memory 3. **Operation Limit Exceeded**: ``` Error: Operation limit exceeded: 100000 operations ``` Solution: Check for infinite loops or reduce computational complexity 4. **Canvas Errors**: ``` Error: Canvas error: Cannot read property 'getContext' of null ``` Solution: Ensure canvas is created before using ### Debugging Tips 1. **Use console.log liberally**: ```javascript const data = [1, 2, 3] console.log('Data:', data) const result = data.map(x => x * 2) console.log('Result:', result) ``` 2. **Break complex code into steps**: ```javascript // Step 1: Generate data const data = _.range(10).map(() => Math.random()) console.log('Generated data:', data.length, 'points') // Step 2: Process data const processed = data.map(x => x * 100) console.log('Processed data range:', _.min(processed), 'to', _.max(processed)) // Step 3: Visualize console.log('Creating visualization...') // ... canvas code ``` 3. **Check execution stats**: - Monitor memory usage for large datasets - Watch operation count for loops - Optimize based on execution time ## Advanced Examples ### 1. Interactive Algorithm Visualization ```javascript // @import lodash // Bubble sort with visualization const canvas = createCanvas(400, 300) const ctx = canvas.getContext('2d') const data = _.shuffle(_.range(1, 21)) // Random array 1-20 const steps = [] // Bubble sort algorithm for (let i = 0; i < data.length; i++) { for (let j = 0; j < data.length - i - 1; j++) { if (data[j] > data[j + 1]) { // Swap [data[j], data[j + 1]] = [data[j + 1], data[j]] steps.push([...data]) // Record step } } } // Visualize final sorted array data.forEach((value, index) => { const barHeight = value * 10 const x = index * 18 + 10 const y = 280 - barHeight ctx.fillStyle = `hsl(${value * 18}, 70%, 50%)` ctx.fillRect(x, y, 15, barHeight) ctx.fillStyle = '#000' ctx.fillText(value, x + 2, y - 2) }) console.log(`Sorting completed in ${steps.length} steps`) return canvas ``` ### 2. Statistical Analysis with Charts ```javascript // @import lodash // @import d3 // Generate sample data const sampleSize = 1000 const data = _.range(sampleSize).map(() => d3.randomNormal(50, 15)() // Normal distribution, mean=50, std=15 ) // Calculate statistics const stats = { count: data.length, mean: d3.mean(data), median: d3.median(data), deviation: d3.deviation(data), min: d3.min(data), max: d3.max(data) } console.log('Statistics:', stats) // Create histogram const canvas = createCanvas(500, 400) const ctx = canvas.getContext('2d') const bins = d3.histogram() .domain(d3.extent(data)) .thresholds(20)(data) const xScale = d3.scaleLinear() .domain(d3.extent(data)) .range([50, 450]) const yScale = d3.scaleLinear() .domain([0, d3.max(bins, d => d.length)]) .range([350, 50]) bins.forEach(bin => { const x = xScale(bin.x0) const y = yScale(bin.length) const width = xScale(bin.x1) - xScale(bin.x0) - 1 const height = 350 - y ctx.fillStyle = '#3498db' ctx.fillRect(x, y, width, height) ctx.fillStyle = '#000' ctx.fillText(bin.length, x + width/2 - 5, y - 5) }) // Add title ctx.fillStyle = '#000' ctx.font = '16px Arial' ctx.fillText('Normal Distribution Histogram', 150, 30) return { stats, canvas } ``` ### 3. Real-time Data Processing ```javascript // @import lodash // @import moment // Simulate time-series data const now = moment() const timePoints = _.range(24).map(hour => ({ time: now.clone().subtract(24 - hour, 'hours'), value: Math.sin(hour * Math.PI / 12) * 50 + 50 + Math.random() * 20 })) // Process data const processed = timePoints.map(point => ({ hour: point.time.format('HH:mm'), value: Math.round(point.value * 100) / 100, trend: point.value > 50 ? 'up' : 'down' })) // Calculate rolling average const windowSize = 3 const rollingAverage = processed.map((point, index) => { const start = Math.max(0, index - windowSize + 1) const window = processed.slice(start, index + 1) const avg = _.meanBy(window, 'value') return { ...point, rollingAvg: Math.round(avg * 100) / 100 } }) console.log('Data points:', processed.length) console.log('Sample:', processed.slice(0, 3)) // Visualize trends const canvas = createCanvas(600, 300) const ctx = canvas.getContext('2d') rollingAverage.forEach((point, index) => { const x = index * 24 + 50 const y = 250 - (point.value * 2) const avgY = 250 - (point.rollingAvg * 2) // Draw data point ctx.fillStyle = point.trend === 'up' ? '#2ecc71' : '#e74c3c' ctx.fillRect(x - 2, y - 2, 4, 4) // Draw rolling average ctx.fillStyle = '#3498db' ctx.fillRect(x - 1, avgY - 1, 2, 2) // Connect points if (index > 0) { const prevX = (index - 1) * 24 + 50 const prevY = 250 - (rollingAverage[index - 1].rollingAvg * 2) ctx.strokeStyle = '#3498db' ctx.beginPath() ctx.moveTo(prevX, prevY) ctx.lineTo(x, avgY) ctx.stroke() } }) return { processed: rollingAverage, canvas } ``` ## Best Practices ### 1. Code Organization - Break complex tasks into smaller functions - Use descriptive variable names - Add comments for complex logic - Return meaningful results ### 2. Performance Optimization - Avoid unnecessary loops - Use efficient algorithms - Monitor memory usage for large datasets - Consider using libraries like lodash for optimized operations ### 3. Error Prevention - Validate inputs before processing - Use try-catch blocks for risky operations - Check array lengths before accessing elements - Handle edge cases explicitly ### 4. Visualization Guidelines - Choose appropriate canvas sizes (400x300 is standard) - Use contrasting colors for better visibility - Add labels and legends when helpful - Scale graphics appropriately for the data ## Security and Limitations ### What's Allowed - All standard JavaScript features - Mathematical computations - Data manipulation and analysis - Canvas graphics and visualizations - Supported library functions - Console output and debugging ### What's Not Allowed - Direct DOM manipulation - Network requests (fetch, XMLHttpRequest) - File system access - Local storage access - WebSocket connections - Worker creation - Eval or Function constructor (except internally) ### Resource Limits - Maximum execution time: 10 seconds - Memory limit: ~50MB - Operation limit: 100,000 operations - Library loading timeout: 30 seconds ## Troubleshooting ### Common Issues 1. **Code doesn't run**: Check for syntax errors 2. **Canvas doesn't display**: Ensure you return the canvas object 3. **Library not loading**: Verify library name and internet connection 4. **Performance issues**: Check operation count and optimize loops ### Getting Help - Use console.log to debug step by step - Check the execution statistics for performance insights - Simplify complex code to isolate issues - Refer to library documentation for specific functions This Code Runner provides a powerful environment for learning, prototyping, and demonstrating JavaScript concepts with real-time feedback and rich visualizations. Experiment with different features and libraries to discover what's possible! ================================================ FILE: docs/dev/conversation_patch_example.js ================================================ /** * Example patch for Conversation.vue to add VFS file upload * * This shows the minimal changes needed to add VFS upload to the chat interface */ // 1. Add these imports to the existing imports section const additionalImports = ` import ChatVFSUploader from '@/components/ChatVFSUploader.vue' import VFSProvider from '@/components/VFSProvider.vue' ` // 2. Add these event handlers to the script section const eventHandlers = ` // VFS event handlers const handleVFSFileUploaded = (fileInfo) => { console.log('File uploaded to VFS:', fileInfo) nui_msg.success(\`File uploaded: \${fileInfo.filename} → \${fileInfo.path}\`) } const handleCodeExampleAdded = (codeInfo) => { // Add code examples as a system message const exampleMessage = \`📁 **Files uploaded successfully!** **Python example:** \\\`\\\`\\\`python \${codeInfo.python} \\\`\\\`\\\` **JavaScript example:** \\\`\\\`\\\`javascript \${codeInfo.javascript} \\\`\\\`\\\` Your files are now available in the Virtual File System.\` // Add system message to chat addChat( sessionUuid, { uuid: uuidv7(), dateTime: nowISO(), text: exampleMessage, inversion: false, error: false, loading: false, }, ) nui_msg.success('Files uploaded! Code examples added to chat.') } ` // 3. Template modification - wrap the entire template with VFSProvider const templateWrapper = ` ` // 4. Add these styles const additionalStyles = ` ` // 5. Complete minimal integration example const minimalIntegrationExample = ` // MINIMAL INTEGRATION: Just add these 3 things to Conversation.vue // 1. Add import import ChatVFSUploader from '@/components/ChatVFSUploader.vue' import VFSProvider from '@/components/VFSProvider.vue' // 2. Add event handler const handleVFSFileUploaded = (fileInfo) => { nui_msg.success(\`File uploaded: \${fileInfo.filename}\`) } // 3. Add to template (wrap existing content with VFSProvider and add uploader) /* */ ` export { additionalImports, eventHandlers, templateWrapper, additionalStyles, minimalIntegrationExample } ================================================ FILE: docs/dev/conversation_vfs_integration.md ================================================ # Chat Session VFS Integration This guide shows how to add VFS file upload functionality to chat sessions so users can upload files and use them directly in code runners. ## Integration Steps ### 1. Add VFS Provider to Chat Layout First, wrap your chat layout with the VFS provider in `web/src/views/chat/layout/Layout.vue`: ```vue ``` ### 2. Modify Conversation Component Update `web/src/views/chat/components/Conversation.vue`: ```vue ``` ### 3. Alternative: Simpler Integration For a cleaner integration, you can add the VFS uploader as a button next to the existing upload button: ```vue ``` ## Usage Flow ### 1. User uploads files via VFS uploader: - Click the VFS upload button (folder icon) - Select files (CSV, JSON, Excel, text files, etc.) - Choose target directory (`/data`, `/workspace`, `/uploads`) - Files are uploaded to the Virtual File System ### 2. System generates code examples: - Automatically generates Python and JavaScript examples - Shows how to access the uploaded files - User can copy the code or add it to chat ### 3. User runs code in chat: ```python # Python example (auto-generated) import pandas as pd df = pd.read_csv('/data/sales.csv') print(f"Loaded {len(df)} rows from sales.csv") print(df.head()) ``` ```javascript // JavaScript example (auto-generated) const fs = require('fs'); const csvContent = fs.readFileSync('/data/sales.csv', 'utf8'); const lines = csvContent.split('\n'); console.log(`Loaded CSV sales.csv with ${lines.length - 1} rows`); ``` ### 4. Files persist throughout the session: - Files remain available in VFS during the entire chat session - Can be accessed by subsequent code runners - Can be downloaded via file manager ## Key Features ### Dual Upload System: - **Server Upload** (existing): Files for chat context, AI can see content - **VFS Upload** (new): Files for code execution, available in runners ### Smart Code Generation: - Detects file types and generates appropriate code - Provides both Python and JavaScript examples - Shows exact file paths for easy copy-paste ### Session Integration: - Files tied to specific chat session - Automatic code example insertion into chat - Visual feedback and success messages ### User Experience: - Clear distinction between upload types - Helpful tooltips and guidance - Immediate code examples - File manager for browsing/downloading ## Complete Example Message Flow 1. **User uploads** `sales_data.csv` via VFS uploader 2. **System responds** with auto-generated message: ``` 📁 Files uploaded successfully! Python example: ```python import pandas as pd df = pd.read_csv('/data/sales_data.csv') print(f"Loaded {len(df)} rows from sales_data.csv") ``` JavaScript example: ```javascript const fs = require('fs'); const csvContent = fs.readFileSync('/data/sales_data.csv', 'utf8'); ``` ``` 3. **User copies and runs** the Python code 4. **User processes data** and saves results to VFS 5. **User downloads** processed files via file manager This creates a complete data processing workflow within the chat interface! ================================================ FILE: docs/dev/python_async_execution.md ================================================ # Running Async Python Code in Pyodide ## Overview This document outlines the challenges and solutions for executing asynchronous Python code in the Pyodide-based Python runner used in this chat application. ## The Problem When users write Python code containing `asyncio.run()` calls, they encounter a runtime error: ``` RuntimeError: asyncio.run() cannot be called from a running event loop ``` This occurs because Pyodide already runs within an active asyncio event loop, and `asyncio.run()` attempts to create a new event loop, which is not allowed. ## Root Cause - **Pyodide Environment**: Pyodide executes Python code within a JavaScript environment that already has an active asyncio event loop - **asyncio.run() Limitation**: This function is designed to be the main entry point for asyncio programs and cannot be called from within an existing event loop - **Common Pattern**: Many Python async tutorials and examples use `if __name__ == "__main__": asyncio.run(main())` which doesn't work in Pyodide ## Solution Implementation ### Detection and Transformation The Python runner now automatically detects and transforms `asyncio.run()` calls: 1. **Pattern Detection**: Code is scanned for `asyncio.run()` calls 2. **Syntax Transformation**: `asyncio.run(func())` is converted to `await func()` 3. **Context Wrapping**: The entire code is wrapped in an async function context 4. **Pyodide Execution**: Uses `pyodide.runPythonAsync()` instead of `pyodide.runPython()` ### Code Transformation Example **Original Code:** ```python import asyncio async def main(): print("Hello from async!") await asyncio.sleep(1) print("Done!") if __name__ == "__main__": asyncio.run(main()) ``` **Transformed Code:** ```python import asyncio async def _execute_main(): import asyncio async def main(): print("Hello from async!") await asyncio.sleep(1) print("Done!") if __name__ == "__main__": await main() # Execute the main function await _execute_main() ``` ### Key Changes in Implementation 1. **Regex Replacement**: `asyncio.run(([^)]+))` → `await $1` 2. **Async Wrapper**: Entire code wrapped in `async def _execute_main():` 3. **Top-level Await**: Uses `await _execute_main()` for execution 4. **Execution Method**: Uses `pyodide.runPythonAsync()` for async code ## Technical Details ### Why This Works - **Pyodide Compatibility**: `runPythonAsync()` is designed to handle top-level await statements - **Event Loop Reuse**: Instead of creating a new event loop, the code runs within Pyodide's existing loop - **Proper Awaiting**: The async function is properly awaited, preventing "coroutine was never awaited" warnings ### Error Handling The runner provides informative feedback: - Detects `asyncio.run()` usage and notifies the user - Explains the transformation being applied - Maintains error context for debugging ## Best Practices ### For Users 1. **Avoid `asyncio.run()`**: In Pyodide environments, use async/await directly 2. **Top-level Async**: Write async functions and let the runner handle execution 3. **Error Awareness**: Understand that Pyodide has different async execution patterns ### For Developers 1. **Detection First**: Always scan for problematic patterns before execution 2. **Clear Messaging**: Inform users when code transformations occur 3. **Fallback Strategy**: Use `runPython()` for synchronous code, `runPythonAsync()` for async 4. **Testing**: Test with various async patterns including nested async calls ## Limitations 1. **Complex Patterns**: Very complex async patterns may still require manual adjustment 2. **Performance**: Code transformation adds slight overhead 3. **Debugging**: Transformed code may be harder to debug than original ## Future Improvements 1. **Better Pattern Recognition**: Handle more complex `asyncio.run()` usage patterns 2. **Source Maps**: Maintain mapping between original and transformed code for better debugging 3. **Optimization**: Cache transformation results for repeated code execution 4. **User Education**: Provide more guidance on async Python patterns in Pyodide ## Conclusion The async Python code execution solution successfully bridges the gap between standard Python async patterns and Pyodide's execution environment. By automatically detecting and transforming `asyncio.run()` calls, users can run async Python code seamlessly without needing to understand the underlying Pyodide constraints. ================================================ FILE: docs/dev/sse_processing_logic.md ================================================ # Server-Sent Events (SSE) Processing Logic This document explains how the chat application handles Server-Sent Events (SSE) streaming responses from the backend. ## Overview The SSE processing logic is implemented in `useStreamHandling.ts` and handles real-time streaming of chat responses. It manages buffering, message parsing, and error handling for continuous data streams. ## SSE Protocol Basics Server-Sent Events follow this format: ``` data: {"id": "chatcmpl-123", "choices": [{"delta": {"content": "Hello"}}]} data: {"id": "chatcmpl-123", "choices": [{"delta": {"content": " world"}}]} data: [DONE] ``` - Each message starts with `data: ` - Messages are separated by double newlines (`\n\n`) - The stream ends with `data: [DONE]` ## Processing Flow ### 1. Stream Reading Setup ```javascript const response = await fetch(streamingUrl, requestConfig) const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' ``` ### 2. Chunk Processing Loop ```javascript while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value, { stream: true }) buffer += chunk // Process complete messages processBuffer(buffer) } ``` ### 3. Buffer Management The key challenge is handling partial messages that arrive across multiple chunks: ```javascript // Input buffer: "data: {partial}\n\ndata: {complete}\n\nda" const lines = buffer.split('\n\n') buffer = lines.pop() || '' // Keep incomplete part: "da" // Process complete messages: ["data: {partial}", "data: {complete}"] for (const line of lines) { if (line.trim()) { processMessage(line) } } // because each delta is acutally a full message // process last one in enough if (lines.length > 0) { onStreamChunk(lines[lines.length - 1], updateIndex) } ``` ### 4. Data Extraction Each SSE message is processed by `extractStreamingData()`: ```javascript function extractStreamingData(streamResponse: string): string { // Handle standard SSE format: "data: {...}" if (streamResponse.startsWith('data:')) { return streamResponse.slice(5).trim() } // Handle multiple segments (fallback) const lastDataPosition = streamResponse.lastIndexOf('\n\ndata:') if (lastDataPosition === -1) { return streamResponse.trim() } return streamResponse.slice(lastDataPosition + 8).trim() } ``` ### 5. Message Processing Once data is extracted, it's parsed and processed: ```javascript function processStreamChunk(chunk: string, responseIndex: number, sessionUuid: string) { const data = extractStreamingData(chunk) if (!data) return try { const parsedData = JSON.parse(data) // Validate structure if (!parsedData.choices?.[0]?.delta?.content || !parsedData.id) { console.warn('Invalid stream chunk structure') return } const content = parsedData.choices[0].delta.content const messageId = parsedData.id.replace('chatcmpl-', '') // Update chat with new content updateChat(sessionUuid, responseIndex, { uuid: messageId, text: content, // ... other properties }) } catch (error) { console.error('Failed to parse stream chunk:', error) } } ``` ## Error Handling ### Stream Errors - HTTP errors (non-2xx responses) - Network connectivity issues - Reader errors ### Parsing Errors - Malformed JSON in SSE data - Invalid message structure - Missing required fields ### Recovery Strategies - Graceful degradation on parse errors - User notification for critical errors - Automatic cleanup of incomplete messages ## Key Functions ### `streamChatResponse()` Main streaming function for new chat messages: - Sets up fetch request with streaming enabled - Manages ReadableStream reader - Handles progressive response processing - Calls progress callbacks for real-time updates ### `streamRegenerateResponse()` Specialized streaming for message regeneration: - Similar to `streamChatResponse()` but with `regenerate: true` - Updates existing message instead of creating new one ### `processStreamChunk()` Core message processing logic: - Extracts JSON data from SSE format - Validates message structure - Updates chat store with new content - Handles artifacts extraction ### `extractStreamingData()` Utility for parsing SSE data: - Removes `data: ` prefix - Handles both single messages and multi-segment responses - Trims whitespace and normalizes output ## Buffer Management Strategy The buffering strategy handles the asynchronous nature of streaming: 1. **Accumulate**: Add incoming chunks to buffer 2. **Split**: Divide buffer by SSE delimiters (`\n\n`) 3. **Process**: Handle complete messages 4. **Retain**: Keep incomplete message for next iteration 5. **Cleanup**: Process remaining buffer when stream ends This ensures no message data is lost and all messages are processed in order. ## Integration Points ### Progress Callbacks ```javascript onProgress?: (chunk: string, responseIndex: number) => void ``` - Called for each processed SSE message - Allows custom handling in different contexts - Used for real-time UI updates ### Chat Store Integration - Updates stored chat messages - Manages message state (loading, error, complete) - Handles message artifacts and metadata ### Error Notification - User-facing error messages - Developer console logging - Graceful fallback behaviors ## Performance Considerations - **Memory**: Buffer size is automatically managed - **Processing**: Minimal parsing overhead per chunk - **Updates**: Batched UI updates prevent excessive re-renders - **Cleanup**: Proper reader resource management ## Example SSE Flow ``` 1. User sends message: "Explain SSE" 2. Server responds with stream: data: {"id":"msg-1","choices":[{"delta":{"content":"Server-Sent"}}]} data: {"id":"msg-1","choices":[{"delta":{"content":" Events"}}]} data: {"id":"msg-1","choices":[{"delta":{"content":" allow..."}}]} 3. Each chunk updates the UI: "Server-Sent" → "Server-Sent Events" → "Server-Sent Events allow..." 4. Stream ends, message is complete ``` This architecture enables real-time chat experiences while maintaining reliability and error resilience. ================================================ FILE: docs/dev/vfs_integration_example.md ================================================ # VFS Integration Example This document shows how to integrate the Virtual File System upload functionality into the existing chat interface. ## Step 1: Add VFS Components to Chat ### Import VFS Components Add these imports to your `Conversation.vue` component: ```typescript // Add to existing imports import VFSIntegration from '@/components/VFSIntegration.vue' ``` ### Add VFS Integration to Template Add the VFS integration component near the upload button area. Here's how to modify the footer section: ```vue ``` ### Add Event Handlers Add these methods to your component: ```typescript // Add to script setup const handleVFSFileUploaded = (fileInfo: any) => { console.log('File uploaded to VFS:', fileInfo) nui_msg.success(`File uploaded to VFS: ${fileInfo.filename}`) } const handleSampleFilesCreated = () => { console.log('Sample files created in VFS') nui_msg.success('Sample files created! Try running the example scripts.') } ``` ## Step 2: Complete Integration Example Here's a complete example of how to add VFS to an existing chat component: ```vue ``` ## Step 3: Alternative Minimal Integration If you prefer a minimal integration, you can add just the upload button: ```vue ``` ## Step 4: User Experience Flow With VFS integration, users can: 1. **Upload Files**: Click "Upload to VFS" button 2. **Select Directory**: Choose `/data`, `/workspace`, `/tmp`, or `/uploads` 3. **Drag & Drop**: Upload multiple files at once 4. **Use in Code**: Access files immediately in Python/JavaScript 5. **Download Results**: Get processed files back ### Example User Workflow: 1. User uploads `sales.csv` to `/data/sales.csv` 2. User runs Python code: ```python import pandas as pd df = pd.read_csv('/data/sales.csv') df['profit_margin'] = df['profit'] / df['revenue'] df.to_csv('/data/analyzed_sales.csv', index=False) ``` 3. User runs JavaScript code: ```javascript const fs = require('fs'); const data = fs.readFileSync('/data/analyzed_sales.csv', 'utf8'); console.log('Analysis complete, rows:', data.split('\n').length); ``` 4. User downloads the processed file via the file manager ## Benefits - **Seamless Integration**: Files work across Python and JavaScript - **No Server Storage**: Everything stays in browser memory - **Rich File Operations**: Full file system API support - **Data Processing Workflows**: Upload → Process → Download - **Session Management**: Save/restore entire file collections This integration transforms the chat interface into a complete data processing environment where users can upload their data, process it with AI-generated code, and download the results. ================================================ FILE: docs/dev/virtual_file_system_plan.md ================================================ # Virtual File System (VFS) Implementation Plan ## Overview This document outlines the implementation plan for a Virtual File System (VFS) that will enable file I/O simulation and data manipulation capabilities in both JavaScript and Python code runners. The VFS will provide a secure, isolated environment for users to work with files and data without accessing the host system. ## Architecture Goals - **Security**: Isolated from host file system with strict validation - **Performance**: Efficient in-memory storage with compression and caching - **Compatibility**: Standard file API compatibility for both Python and JavaScript - **Persistence**: Session-based storage with import/export capabilities - **Resource Management**: Configurable limits and quota enforcement ## Phase 1: Core Architecture Design ### VFS Class Structure ```javascript class VirtualFileSystem { constructor() { this.files = new Map() // file path -> file data this.directories = new Set() // directory paths this.metadata = new Map() // file metadata (size, modified, etc.) this.currentDirectory = '/' // current working directory this.maxFileSize = 10 * 1024 * 1024 // 10MB per file this.maxTotalSize = 100 * 1024 * 1024 // 100MB total this.fileHandlers = new Map() // extension -> handler this.permissions = new Map() // path -> permissions } } ``` ### File System API Design ```javascript // Core operations fs.writeFile(path, data, options) // Write file with encoding support fs.readFile(path, encoding) // Read file with optional encoding fs.mkdir(path, recursive) // Create directory fs.rmdir(path, recursive) // Remove directory fs.unlink(path) // Delete file fs.exists(path) // Check if path exists fs.stat(path) // Get file/directory metadata fs.readdir(path) // List directory contents // Navigation fs.chdir(path) // Change current directory fs.getcwd() // Get current working directory // Advanced operations fs.copy(src, dest) // Copy file or directory fs.move(src, dest) // Move/rename file or directory fs.glob(pattern) // Find files by pattern fs.watch(path, callback) // Watch for changes ``` ### Path Resolution System ```javascript class PathResolver { normalize(path) // Handle ./ ../ ~ / normalization resolve(path) // Convert relative to absolute paths validate(path) // Security checks, prevent traversal attacks split(path) // Break path into components join(...parts) // Join path components safely dirname(path) // Get directory portion basename(path) // Get filename portion extname(path) // Get file extension } ``` ## Phase 2: Core Implementation ### File Storage Strategy - **Text Files**: UTF-8 strings with BOM detection - **Binary Files**: Base64 encoded with MIME type detection - **Large Files**: Chunked storage with LZ4 compression - **Memory Management**: LRU cache with configurable size limits - **Metadata Storage**: Created/modified timestamps, permissions, checksums ### Security Model ```javascript class VFSSecurity { validatePath(path) // Prevent directory traversal checkQuota(size) // Enforce storage limits sanitizeFilename(name) // Remove dangerous characters validateFileType(data, ext) // Prevent type confusion attacks enforcePermissions(path, op) // Check read/write permissions scanContent(data) // Basic malware detection } ``` ### Resource Management - Maximum file size: 10MB per file - Maximum total storage: 100MB per session - Maximum files: 1000 per session - Path length: 260 characters maximum - Filename restrictions: No control characters, reserved names ## Phase 3: Python Runner Integration ### Custom File Handlers ```python import io import os import builtins from pathlib import Path class VFSFile(io.TextIOWrapper): """File-like object that interfaces with VFS""" def __init__(self, path, mode, vfs_instance): self.vfs = vfs_instance self.path = path self.mode = mode self.position = 0 self.closed = False def read(self, size=-1): return self.vfs.readFile(self.path, size, self.position) def write(self, data): return self.vfs.writeFile(self.path, data, self.mode) def seek(self, position): self.position = position def tell(self): return self.position ``` ### Python Standard Library Integration ```python # Override built-in open() function original_open = builtins.open def vfs_open(file, mode='r', **kwargs): """VFS-aware open() replacement""" if _is_vfs_path(file): return VFSFile(file, mode, global_vfs) return original_open(file, mode, **kwargs) builtins.open = vfs_open # Patch os module functions import os original_os_functions = {} def patch_os_module(): """Patch os module to work with VFS""" original_os_functions['listdir'] = os.listdir original_os_functions['makedirs'] = os.makedirs original_os_functions['path.exists'] = os.path.exists os.listdir = lambda path: global_vfs.readdir(path) os.makedirs = lambda path, **kwargs: global_vfs.mkdir(path, True) os.path.exists = lambda path: global_vfs.exists(path) # Patch pathlib.Path for modern Python code # Patch csv, json, pickle modules for data access ``` ### Data Science Library Support ```python # Pandas integration import pandas as pd original_read_csv = pd.read_csv original_to_csv = pd.DataFrame.to_csv pd.read_csv = lambda filepath, **kwargs: original_read_csv( global_vfs.readFile(filepath) if _is_vfs_path(filepath) else filepath, **kwargs ) # NumPy, SciPy, Matplotlib file operations # Jupyter notebook file operations ``` ## Phase 4: JavaScript Runner Integration ### Node.js-style fs Module ```javascript const fs = { // Promise-based async versions readFile: async (path, options = {}) => { const encoding = options.encoding || options return await vfs.readFile(path, encoding) }, writeFile: async (path, data, options = {}) => { return await vfs.writeFile(path, data, options) }, mkdir: async (path, options = {}) => { return await vfs.mkdir(path, options.recursive) }, readdir: async (path, options = {}) => { return await vfs.readdir(path, options) }, // Synchronous versions readFileSync: (path, options = {}) => { const encoding = options.encoding || options return vfs.readFileSync(path, encoding) }, writeFileSync: (path, data, options = {}) => { return vfs.writeFileSync(path, data, options) }, mkdirSync: (path, options = {}) => { return vfs.mkdirSync(path, options.recursive) }, readdirSync: (path, options = {}) => { return vfs.readdirSync(path, options) }, // Stream support createReadStream: (path, options = {}) => { return new VFSReadableStream(path, options) }, createWriteStream: (path, options = {}) => { return new VFSWritableStream(path, options) }, // Additional utilities existsSync: (path) => vfs.exists(path), statSync: (path) => vfs.stat(path), unlinkSync: (path) => vfs.unlink(path), rmdirSync: (path, options = {}) => vfs.rmdir(path, options.recursive) } ``` ### Stream Implementation ```javascript class VFSReadableStream extends ReadableStream { constructor(path, options = {}) { super({ start(controller) { // Initialize stream with VFS file data }, pull(controller) { // Read chunks from VFS }, cancel() { // Cleanup } }) } } class VFSWritableStream extends WritableStream { constructor(path, options = {}) { super({ write(chunk, controller) { // Write chunk to VFS }, close() { // Finalize file in VFS }, abort(reason) { // Cleanup on error } }) } } ``` ## Phase 5: Data Format Support ### File Type Handlers ```javascript const FileHandlers = { // Text formats '.txt': new TextHandler(), '.csv': new CSVHandler(), '.json': new JSONHandler(), '.xml': new XMLHandler(), '.md': new MarkdownHandler(), '.yaml': new YAMLHandler(), '.toml': new TOMLHandler(), // Data formats '.xlsx': new ExcelHandler(), '.parquet': new ParquetHandler(), '.sqlite': new SQLiteHandler(), '.h5': new HDF5Handler(), // Binary formats '.png': new ImageHandler(), '.jpg': new ImageHandler(), '.gif': new ImageHandler(), '.pdf': new PDFHandler(), '.zip': new ZipHandler(), '.tar': new TarHandler(), // Programming languages '.py': new PythonHandler(), '.js': new JavaScriptHandler(), '.html': new HTMLHandler(), '.css': new CSSHandler() } class CSVHandler { async parse(data, options = {}) { // Parse CSV with configurable delimiter, headers, etc. const delimiter = options.delimiter || ',' const hasHeaders = options.headers !== false // Return structured data } async stringify(data, options = {}) { // Convert structured data to CSV string } } class JSONHandler { async parse(data) { return JSON.parse(data) } async stringify(data, options = {}) { const indent = options.indent || 2 return JSON.stringify(data, null, indent) } } ``` ### Data Import/Export System ```javascript class DataImporter { async importFromURL(url, path, options = {}) { // Fetch remote file and store in VFS const response = await fetch(url) const data = await response.arrayBuffer() return await vfs.writeFile(path, data, { binary: true }) } async importFromFile(file, path) { // Handle browser File API uploads const reader = new FileReader() return new Promise((resolve, reject) => { reader.onload = async (e) => { await vfs.writeFile(path, e.target.result) resolve(path) } reader.onerror = reject reader.readAsArrayBuffer(file) }) } async importCSV(csvText, path, options = {}) { const handler = new CSVHandler() const data = await handler.parse(csvText, options) await vfs.writeFile(path, JSON.stringify(data)) return data } async importJSON(jsonText, path) { const data = JSON.parse(jsonText) await vfs.writeFile(path, jsonText) return data } // Export functions async exportToDownload(path, filename) { const data = await vfs.readFile(path, 'binary') const blob = new Blob([data]) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename a.click() URL.revokeObjectURL(url) } async exportToZip(paths, zipName) { // Create ZIP file containing multiple VFS files const JSZip = await import('jszip') const zip = new JSZip() for (const path of paths) { const data = await vfs.readFile(path, 'binary') const relativePath = path.startsWith('/') ? path.slice(1) : path zip.file(relativePath, data) } const zipBlob = await zip.generateAsync({ type: 'blob' }) this.downloadBlob(zipBlob, zipName) } async exportToDataURL(path) { const data = await vfs.readFile(path, 'binary') const mimeType = this.detectMimeType(path) return `data:${mimeType};base64,${btoa(data)}` } } ``` ## Phase 6: Advanced Features ### File System Utilities ```javascript class FileSystemUtils { // Search and filtering async find(pattern, options = {}) { // Find files by glob pattern const recursive = options.recursive !== false const maxDepth = options.maxDepth || 100 const caseInsensitive = options.caseInsensitive || false return vfs.glob(pattern, { recursive, maxDepth, caseInsensitive }) } async grep(pattern, files, options = {}) { // Search within files for text patterns const results = [] const regex = new RegExp(pattern, options.flags || 'gi') for (const file of files) { const content = await vfs.readFile(file, 'utf8') const matches = [...content.matchAll(regex)] if (matches.length > 0) { results.push({ file, matches }) } } return results } // File operations async compress(path, algorithm = 'gzip') { const data = await vfs.readFile(path, 'binary') const compressed = await this.compressData(data, algorithm) const compressedPath = `${path}.${algorithm}` await vfs.writeFile(compressedPath, compressed, { binary: true }) return compressedPath } async decompress(path) { const data = await vfs.readFile(path, 'binary') const algorithm = this.detectCompressionType(path) const decompressed = await this.decompressData(data, algorithm) const originalPath = path.replace(new RegExp(`\.${algorithm}$`), '') await vfs.writeFile(originalPath, decompressed, { binary: true }) return originalPath } async checksum(path, algorithm = 'sha256') { const data = await vfs.readFile(path, 'binary') const hash = await crypto.subtle.digest(algorithm.toUpperCase(), data) return Array.from(new Uint8Array(hash)) .map(b => b.toString(16).padStart(2, '0')) .join('') } // Batch operations async bulkCopy(srcPattern, destDir, options = {}) { const files = await this.find(srcPattern) const results = [] for (const file of files) { const basename = vfs.path.basename(file) const destPath = vfs.path.join(destDir, basename) await vfs.copy(file, destPath) results.push({ src: file, dest: destPath }) } return results } async bulkDelete(pattern, options = {}) { const files = await this.find(pattern) const confirm = options.confirm !== false if (confirm && files.length > 10) { // Safety check for bulk deletion throw new Error(`Bulk delete would affect ${files.length} files. Use {confirm: false} to proceed.`) } for (const file of files) { await vfs.unlink(file) } return files } async bulkRename(pattern, replacement, options = {}) { const files = await this.find(pattern) const results = [] for (const file of files) { const newName = file.replace(new RegExp(pattern), replacement) await vfs.move(file, newName) results.push({ old: file, new: newName }) } return results } } ``` ### Session Persistence ```javascript class VFSPersistence { constructor(vfs) { this.vfs = vfs this.storageKey = 'vfs_session' this.autoSaveInterval = null } async saveSession(name = 'default') { // Serialize VFS state to JSON const sessionData = { version: '1.0', timestamp: new Date().toISOString(), files: Object.fromEntries(this.vfs.files), directories: Array.from(this.vfs.directories), metadata: Object.fromEntries(this.vfs.metadata), currentDirectory: this.vfs.currentDirectory } // Compress session data const compressed = await this.compressSession(sessionData) // Store in IndexedDB for persistence await this.storeInIndexedDB(name, compressed) return { name, size: compressed.length, timestamp: sessionData.timestamp } } async loadSession(name = 'default') { // Load from IndexedDB const compressed = await this.loadFromIndexedDB(name) if (!compressed) { throw new Error(`Session '${name}' not found`) } // Decompress and parse const sessionData = await this.decompressSession(compressed) // Restore VFS state this.vfs.files = new Map(Object.entries(sessionData.files)) this.vfs.directories = new Set(sessionData.directories) this.vfs.metadata = new Map(Object.entries(sessionData.metadata)) this.vfs.currentDirectory = sessionData.currentDirectory return sessionData } async exportSession(name = 'default') { // Export session as downloadable ZIP const sessionData = await this.saveSession(name) const blob = new Blob([JSON.stringify(sessionData)], { type: 'application/json' }) const filename = `vfs_session_${name}_${new Date().toISOString().slice(0, 19)}.json` this.downloadBlob(blob, filename) return filename } async importSession(file) { // Import session from uploaded file const text = await file.text() const sessionData = JSON.parse(text) // Validate session data structure this.validateSessionData(sessionData) // Restore VFS state await this.restoreSessionData(sessionData) return sessionData } enableAutoSave(interval = 30000) { // Automatically save session every 30 seconds if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval) } this.autoSaveInterval = setInterval(async () => { try { await this.saveSession('autosave') } catch (error) { console.warn('Auto-save failed:', error) } }, interval) } disableAutoSave() { if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval) this.autoSaveInterval = null } } async listSessions() { // List all saved sessions const db = await this.openIndexedDB() const transaction = db.transaction(['sessions'], 'readonly') const store = transaction.objectStore('sessions') const keys = await store.getAllKeys() const sessions = [] for (const key of keys) { const session = await store.get(key) sessions.push({ name: key, timestamp: session.timestamp, size: session.size }) } return sessions } async deleteSession(name) { // Delete a saved session const db = await this.openIndexedDB() const transaction = db.transaction(['sessions'], 'readwrite') const store = transaction.objectStore('sessions') await store.delete(name) } } ``` ## Phase 7: Security & Validation ### Security Implementation ```javascript class VFSSecurity { constructor() { this.maxPathLength = 260 this.maxFilenameLength = 255 this.forbiddenChars = /[<>:"|?*\x00-\x1f]/ this.reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'] } validatePath(path) { // Prevent directory traversal attacks if (path.includes('..')) { throw new Error('Path traversal detected') } if (path.length > this.maxPathLength) { throw new Error(`Path too long: ${path.length} > ${this.maxPathLength}`) } // Normalize path separators const normalizedPath = path.replace(/\\/g, '/') // Check for dangerous patterns if (normalizedPath.match(/\/\.{2,}\//)) { throw new Error('Invalid path pattern detected') } return normalizedPath } checkQuota(currentSize, additionalSize) { // Enforce storage limits const totalSize = currentSize + additionalSize if (additionalSize > this.maxFileSize) { throw new Error(`File too large: ${additionalSize} > ${this.maxFileSize}`) } if (totalSize > this.maxTotalSize) { throw new Error(`Storage quota exceeded: ${totalSize} > ${this.maxTotalSize}`) } return true } sanitizeFilename(name) { // Remove dangerous characters from filenames let sanitized = name.replace(this.forbiddenChars, '_') // Check against reserved names const baseName = sanitized.split('.')[0].toUpperCase() if (this.reservedNames.includes(baseName)) { sanitized = `_${sanitized}` } // Ensure filename isn't too long if (sanitized.length > this.maxFilenameLength) { const ext = sanitized.split('.').pop() const base = sanitized.slice(0, this.maxFilenameLength - ext.length - 1) sanitized = `${base}.${ext}` } return sanitized } validateFileType(data, extension) { // Prevent type confusion attacks const detectedType = this.detectFileType(data) const expectedType = this.getExpectedType(extension) if (detectedType && expectedType && detectedType !== expectedType) { console.warn(`File type mismatch: expected ${expectedType}, detected ${detectedType}`) } // Check for executable content if (this.containsExecutableContent(data)) { throw new Error('Executable content detected') } return true } detectFileType(data) { // Basic file type detection by magic bytes const bytes = new Uint8Array(data.slice(0, 16)) // PNG if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) { return 'png' } // JPEG if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) { return 'jpeg' } // PDF if (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) { return 'pdf' } // ZIP if (bytes[0] === 0x50 && bytes[1] === 0x4B) { return 'zip' } return null } containsExecutableContent(data) { // Basic check for executable content patterns const text = typeof data === 'string' ? data : new TextDecoder().decode(data) // Check for common script patterns const dangerousPatterns = [ / ================================================ FILE: mobile/web/manifest.json ================================================ { "name": "chat_mobile", "short_name": "chat_mobile", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: mobile/windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: mobile/windows/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.14) project(chat_mobile LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "chat_mobile") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() # Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: mobile/windows/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # Set fallback configurations for older versions of the flutter tool. if (NOT DEFINED FLUTTER_TARGET_PLATFORM) set(FLUTTER_TARGET_PLATFORM "windows-x64") endif() # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: mobile/windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" void RegisterPlugins(flutter::PluginRegistry* registry) { } ================================================ FILE: mobile/windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: mobile/windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: mobile/windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) # Define the application target. To change its name, change BINARY_NAME in the # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer # work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add preprocessor definitions for the build version. target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: mobile/windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else #define VERSION_AS_NUMBER 1,0,0,0 #endif #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" VALUE "FileDescription", "chat_mobile" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "chat_mobile" "\0" VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "chat_mobile.exe" "\0" VALUE "ProductName", "chat_mobile" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: mobile/windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); // Flutter can complete the first frame before the "show window" callback is // registered. The following call ensures a frame is pending to ensure the // window is shown. It is a no-op if the first frame hasn't completed yet. flutter_controller_->ForceRedraw(); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: mobile/windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: mobile/windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.Create(L"chat_mobile", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: mobile/windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: mobile/windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: mobile/windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } unsigned int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr) -1; // remove the trailing null character int input_length = (int)wcslen(utf16_string); std::string utf8_string; if (target_length == 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: mobile/windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: mobile/windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include #include "resource.h" namespace { /// Window attribute that enables dark mode window decorations. /// /// Redefined in case the developer's machine has a Windows SDK older than /// version 10.0.22000.0. /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; /// Registry key for app theme preference. /// /// A value of 0 indicates apps should use dark mode. A non-zero or missing /// value indicates apps should use light mode. constexpr const wchar_t kGetPreferredBrightnessRegKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); } FreeLibrary(user32_module); } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::Create(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } UpdateTheme(window); return OnCreate(); } bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; case WM_DWMCOLORIZATIONCOLORCHANGED: UpdateTheme(hwnd); return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } void Win32Window::UpdateTheme(HWND const window) { DWORD light_mode; DWORD light_mode_size = sizeof(light_mode); LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, &light_mode, &light_mode_size); if (result == ERROR_SUCCESS) { BOOL enable_dark_mode = light_mode == 0; DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &enable_dark_mode, sizeof(enable_dark_mode)); } } ================================================ FILE: mobile/windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size this function will scale the inputted width and height as // as appropriate for the default monitor. The window is invisible until // |Show| is called. Returns true if the window was created successfully. bool Create(const std::wstring& title, const Point& origin, const Size& size); // Show the current window. Returns true if the window was successfully shown. bool Show(); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; // Update the window frame's theme to match the system theme. static void UpdateTheme(HWND const window); bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_ ================================================ FILE: scripts/branch_clean.py ================================================ import subprocess from datetime import datetime def get_local_branches(): # Get the list of local branches result = subprocess.run(['git', 'branch'], stdout=subprocess.PIPE, text=True) branches = result.stdout.splitlines() # Remove the '*' from the current branch branches = [branch.strip('*').strip() for branch in branches] return branches def get_branch_last_commit_date(branch): # Get the last commit date of the branch result = subprocess.run(['git', 'log', '-1', '--format=%cd', '--date=iso', branch], stdout=subprocess.PIPE, text=True) date_str = result.stdout.strip() return datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S %z') def delete_branch(branch): # Delete the branch subprocess.run(['git', 'branch', '-D', branch]) def confirm_deletion(branch): # Ask the user to confirm deletion response = input(f"Do you want to delete the branch '{branch}'? (y/n): ").strip().lower() return response == 'y' def main(): # Get all local branches branches = get_local_branches() # Get the last commit date for each branch branch_dates = [(branch, get_branch_last_commit_date(branch)) for branch in branches] # Sort branches by last commit date (oldest first) branch_dates.sort(key=lambda x: x[1]) # Get the oldest 5 branches oldest_branches = [branch for branch, _ in branch_dates[:5]] # Delete the oldest 5 branches with confirmation for branch in oldest_branches: if confirm_deletion(branch): print(f"Deleting branch: {branch}") delete_branch(branch) else: print(f"Skipping branch: {branch}") if __name__ == "__main__": main() ================================================ FILE: scripts/locale_missing_key.py ================================================ from pathlib import Path import json import argparse # Recursive function to find missing keys in dictionaries def find_missing_keys(base_dict, other_dict): missing_keys = {} for key in base_dict: if key not in other_dict: missing_keys[key] = base_dict[key] elif isinstance(base_dict[key], dict) and isinstance(other_dict[key], dict): sub_missing_keys = find_missing_keys(base_dict[key], other_dict[key]) if sub_missing_keys: missing_keys[key] = sub_missing_keys return missing_keys def check_locales(dir_name: str, base_locale: str = 'zh-CN'): # Load the zh-CN JSON file zh_cn_file = Path(dir_name) / f'{base_locale}.json' with zh_cn_file.open('r') as f: zh_cn = json.load(f) # Look for other JSON files in the current directory for file in Path(dir_name).glob('*.json'): if 'more' in file.stem: continue cur_locale = file.stem if cur_locale != base_locale: with file.open('r') as f: other_dict = json.load(f) missing_keys = find_missing_keys(zh_cn, other_dict) print(f'\n\n please translate to {cur_locale}:', missing_keys) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Check missing keys in language localization files') parser.add_argument('dir_name', type=str, help='directory where the JSON files are located') parser.add_argument('--base', type=str, default='zh-CN', help='base locale to compare against') args = parser.parse_args() check_locales(args.dir_name, args.base) # python check_locales.py /path/to/locales --base en-US ================================================ FILE: scripts/merge_keys.py ================================================ import json from pathlib import Path # Function to recursively merge two dictionaries def merge_dicts(d1, d2): for key, val2 in d2.items(): if key in d1: # If both values are dictionaries, merge them recursively if isinstance(val2, dict) and isinstance(d1[key], dict): merge_dicts(d1[key], val2) # If both values are lists, extend the first list with the second elif isinstance(val2, list) and isinstance(d1[key], list): d1[key].extend(val2) # Otherwise, overwrite the first value with the second else: d1[key] = val2 else: # If the key doesn't exist in the first dict, add it and its value d1[key] = val2 def merge_json_files(file1, file2): """Merges the contents of two JSON files recursively by key.""" # Read in the JSON data from the files with open(file1, 'r') as f1: data1 = json.load(f1) with open(file2, 'r') as f2: data2 = json.load(f2) merge_dicts(data1, data2) # write the merged content back to file2 with open(file1, 'w') as fp1: json.dump(data1, fp1, indent=4,ensure_ascii=False, sort_keys=True) # main locale_dir = Path(__file__).parent.parent / "web/src/locales" extra_jsons = locale_dir.glob("*-more.json") # web/src/locales/en-US.json web/src/locales/en-US-more.json for extra in extra_jsons: print(extra) origin = extra.parent / extra.name.replace('-more', '') print(origin, extra) merge_json_files(origin, extra) ================================================ FILE: scripts/remove_older_branch.py ================================================ import subprocess import time # Get the list of merged branches output = subprocess.check_output(['git', 'branch']).decode('utf-8') print(output) # Exclude the current branch and any branches named "main", "master", or "develop" exclude_branches = ['main', 'master', 'develop'] branches = [line.strip('* ') for line in output.split('\n') if line.strip('* ') not in exclude_branches and line.strip()] print(branches) # Get the current time one month ago one_month_ago = time.time() - 30 * 24 * 60 * 60 # Delete branches that have not been updated in the last month for branch in branches: # Get the Unix timestamp of the last commit on the branch output = subprocess.check_output(['git', 'log', '-1', '--format=%at', branch]).decode('utf-8') print(output) last_commit_time = int(output.strip()) # Delete the branch if its last commit is older than one month if last_commit_time < one_month_ago: subprocess.call(['git', 'branch', '-D', branch]) ================================================ FILE: web/.commitlintrc.json ================================================ { "extends": ["@commitlint/config-conventional"] } ================================================ FILE: web/.editorconfig ================================================ # Editor configuration, see http://editorconfig.org root = true [*] charset = utf-8 indent_style = tab indent_size = 2 end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: web/.eslintrc.cjs ================================================ module.exports = { root: true, extends: ['@antfu'], } ================================================ FILE: web/.gitattributes ================================================ "*.vue" eol=lf "*.js" eol=lf "*.ts" eol=lf "*.jsx" eol=lf "*.tsx" eol=lf "*.cjs" eol=lf "*.cts" eol=lf "*.mjs" eol=lf "*.mts" eol=lf "*.json" eol=lf "*.html" eol=lf "*.css" eol=lf "*.less" eol=lf "*.scss" eol=lf "*.sass" eol=lf "*.styl" eol=lf "*.md" eol=lf ================================================ FILE: web/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules .DS_Store dist dist-ssr coverage *.local /cypress/videos/ /cypress/screenshots/ # Editor directories and files .vscode/* !.vscode/settings.json !.vscode/extensions.json .idea *.suo *.ntvs* *.njsproj *.sln *.sw? # Environment variables files /service/.env ================================================ FILE: web/.husky/commit-msg ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx --no -- commitlint --edit ================================================ FILE: web/.husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-staged ================================================ FILE: web/.vscode/extensions.json ================================================ { "recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"] } ================================================ FILE: web/.vscode/settings.json ================================================ { "prettier.enable": false, "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "html", "json", "jsonc", "json5", "yaml", "yml", "markdown" ], "cSpell.words": [ "antfu", "axios", "bumpp", "chatgpt", "chenzhaoyu", "commitlint", "davinci", "dockerhub", "esno", "GPTAPI", "highlightjs", "hljs", "iconify", "katex", "katexmath", "linkify", "logprobs", "mdhljs", "nodata", "OPENAI", "pinia", "Popconfirm", "rushstack", "Sider", "tailwindcss", "traptitech", "tsup", "Typecheck", "unplugin", "VITE", "vueuse", "Zhao" ], "i18n-ally.enabledParsers": [ "ts" ], "i18n-ally.sortKeys": true, "i18n-ally.keepFulfilled": true, "i18n-ally.localesPaths": [ "src/locales" ], "i18n-ally.keystyle": "nested", "editor.fontFamily": "'Fira Code'" } ================================================ FILE: web/docker-compose/docker-compose.yml ================================================ version: '3' services: app: image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可 ports: - 3002:3002 environment: # 二选一 OPENAI_API_KEY: xxxx # 二选一 OPENAI_ACCESS_TOKEN: xxxxxx # API接口地址,可选,设置 OPENAI_API_KEY 时可用 OPENAI_API_BASE_URL: xxxx # API模型,可选,设置 OPENAI_API_KEY 时可用 OPENAI_API_MODEL: xxxx # 反向代理,可选 API_REVERSE_PROXY: xxx # 访问权限密钥,可选 AUTH_SECRET_KEY: xxx # 超时,单位毫秒,可选 TIMEOUT_MS: 60000 # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 SOCKS_PROXY_HOST: xxxx # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 SOCKS_PROXY_PORT: xxxx nginx: image: nginx:alpine ports: - '80:80' expose: - '80' volumes: - ./nginx/html:/usr/share/nginx/html - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf links: - app ================================================ FILE: web/docker-compose/nginx/nginx.conf ================================================ server { listen 80; server_name localhost; charset utf-8; error_page 500 502 503 504 /50x.html; location / { root /usr/share/nginx/html; try_files $uri /index.html; } location /api { proxy_set_header X-Real-IP $remote_addr; #转发用户IP proxy_pass http://app:3002; } proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } ================================================ FILE: web/docker-compose/readme.md ================================================ ### docker-compose 部署教程 - 将打包好的前端文件放到 `nginx/html` 目录下 - ```shell # 启动 docker-compose up -d ``` - ```shell # 查看运行状态 docker ps ``` - ```shell # 结束运行 docker-compose down ``` ================================================ FILE: web/docs/code_runner.md ================================================ # Code Runner Feature Implementation Plan ## Overview This document outlines the implementation plan for adding interactive code execution capabilities to the chat application's artifact system. The code runner will allow users to execute JavaScript, Python, and other languages directly within chat artifacts, providing real-time output and interactive programming experiences. ## Goals - **Educational**: Enable live coding tutorials and learning experiences - **Prototyping**: Quick algorithm testing and experimentation - **Data Visualization**: Interactive charts, graphs, and visual demonstrations - **Development**: Code examples that users can run and modify ## Architecture Decision ### Selected Approach: Browser-Based Execution **Reasoning**: - No server resources required - Real-time execution with minimal latency - Scales with user count (computation on client) - Supports rich DOM manipulation and visualization - Easier deployment and maintenance **Supported Languages**: 1. **JavaScript/TypeScript** - Native browser execution in Web Workers 2. **Python** - Pyodide (Python in WebAssembly) 3. **Future**: HTML/CSS live preview, SQL (via SQLite WASM) ## Technical Implementation ### 1. Database Schema Extensions ```sql -- Add execution results to chat messages ALTER TABLE chat_message ADD COLUMN execution_results JSONB DEFAULT '[]'; -- Index for searching executable artifacts CREATE INDEX idx_chat_message_execution ON chat_message USING GIN (execution_results) WHERE execution_results != '[]'; ``` **Execution Result Structure**: ```json { "artifact_uuid": "string", "execution_id": "string", "timestamp": "ISO8601", "language": "javascript|python", "output": [ { "type": "log|error|return|stdout", "content": "string", "timestamp": "ISO8601" } ], "execution_time_ms": "number", "status": "success|error|timeout" } ``` ### 2. Frontend Architecture #### Enhanced Artifact Types **New Artifact Type**: `executable-code` - Extends existing `code` artifacts - Includes execution metadata - Supports multiple output formats ```typescript // web/src/typings/chat.d.ts interface ExecutableArtifact extends Artifact { type: 'executable-code' language: 'javascript' | 'python' | 'typescript' isExecutable: true executionResults?: ExecutionResult[] } interface ExecutionResult { id: string type: 'log' | 'error' | 'return' | 'stdout' content: string timestamp: string } ``` #### Core Components **1. Enhanced ArtifactViewer.vue** ```vue ``` **2. Code Runner Service** ```typescript // web/src/services/codeRunner.ts import { ExecutionResult } from '@/typings/chat' export class CodeRunner { private jsWorker: Worker | null = null private pythonRunner: PythonRunner | null = null async execute(language: string, code: string): Promise { const startTime = performance.now() try { let results: ExecutionResult[] switch (language) { case 'javascript': case 'typescript': results = await this.executeJavaScript(code) break case 'python': results = await this.executePython(code) break default: throw new Error(`Unsupported language: ${language}`) } // Add execution time to results const executionTime = performance.now() - startTime results.forEach(result => { result.execution_time_ms = executionTime }) return results } catch (error) { return [{ id: Date.now().toString(), type: 'error', content: error.message, timestamp: new Date().toISOString(), execution_time_ms: performance.now() - startTime }] } } private async executeJavaScript(code: string): Promise { return new Promise((resolve, reject) => { if (!this.jsWorker) { this.jsWorker = new Worker('/workers/jsRunner.js') } const timeout = setTimeout(() => { this.jsWorker?.terminate() this.jsWorker = null reject(new Error('Code execution timed out')) }, 10000) // 10 second timeout this.jsWorker.onmessage = (e) => { clearTimeout(timeout) const { type, data } = e.data if (type === 'results') { resolve(data) } else if (type === 'error') { reject(new Error(data.message)) } } this.jsWorker.onerror = (error) => { clearTimeout(timeout) reject(error) } this.jsWorker.postMessage({ code, timeout: 10000 }) }) } private async executePython(code: string): Promise { if (!this.pythonRunner) { this.pythonRunner = new PythonRunner() await this.pythonRunner.initialize() } return this.pythonRunner.execute(code) } dispose() { if (this.jsWorker) { this.jsWorker.terminate() this.jsWorker = null } if (this.pythonRunner) { this.pythonRunner.dispose() this.pythonRunner = null } } } ``` **3. JavaScript Worker** ```typescript // public/workers/jsRunner.js class SafeJSRunner { constructor() { this.output = [] this.setupConsole() } setupConsole() { // Capture console methods this.console = { log: (...args) => this.addOutput('log', args.join(' ')), error: (...args) => this.addOutput('error', args.join(' ')), warn: (...args) => this.addOutput('warn', args.join(' ')), info: (...args) => this.addOutput('info', args.join(' ')) } } addOutput(type, content) { this.output.push({ id: Date.now().toString() + Math.random(), type: type, content: String(content), timestamp: new Date().toISOString() }) } async execute(code) { this.output = [] try { // Create safe execution context const safeGlobals = { console: this.console, Math: Math, Date: Date, Array: Array, Object: Object, String: String, Number: Number, Boolean: Boolean, JSON: JSON, setTimeout: (fn, ms) => setTimeout(fn, Math.min(ms, 5000)), setInterval: (fn, ms) => setInterval(fn, Math.max(ms, 100)) } // Execute code in safe context const result = new Function( ...Object.keys(safeGlobals), ` "use strict"; ${code} ` )(...Object.values(safeGlobals)) // Add return value if exists if (result !== undefined) { this.addOutput('return', JSON.stringify(result, null, 2)) } return this.output } catch (error) { this.addOutput('error', error.message) return this.output } } } const runner = new SafeJSRunner() self.onmessage = async (e) => { const { code, timeout } = e.data try { const results = await runner.execute(code) self.postMessage({ type: 'results', data: results }) } catch (error) { self.postMessage({ type: 'error', data: { message: error.message } }) } } ``` **4. Python Runner (Pyodide)** ```typescript // web/src/services/pythonRunner.ts export class PythonRunner { private pyodide: any = null private initialized = false async initialize() { if (this.initialized) return // Dynamic import to avoid bundling const { loadPyodide } = await import('pyodide') this.pyodide = await loadPyodide({ indexURL: 'https://cdn.jsdelivr.net/pyodide/' }) // Install common packages await this.pyodide.loadPackage(['numpy', 'matplotlib', 'pandas']) this.initialized = true } async execute(code: string): Promise { if (!this.initialized) { await this.initialize() } const output: ExecutionResult[] = [] try { // Capture stdout this.pyodide.runPython(` import sys from io import StringIO # Capture stdout old_stdout = sys.stdout sys.stdout = captured_output = StringIO() `) // Execute user code const result = this.pyodide.runPython(code) // Get captured output const stdout = this.pyodide.runPython(` sys.stdout = old_stdout captured_output.getvalue() `) // Add stdout if any if (stdout.trim()) { output.push({ id: Date.now().toString(), type: 'stdout', content: stdout, timestamp: new Date().toISOString() }) } // Add return value if exists if (result !== undefined && result !== null) { output.push({ id: Date.now().toString() + '1', type: 'return', content: String(result), timestamp: new Date().toISOString() }) } return output } catch (error) { return [{ id: Date.now().toString(), type: 'error', content: error.message, timestamp: new Date().toISOString() }] } } dispose() { // Cleanup if needed this.pyodide = null this.initialized = false } } ``` ### 3. Backend Integration #### Artifact Detection Enhancement ```go // api/chat_main_service.go func extractArtifacts(content string) []Artifact { // Existing artifact extraction logic... // Add executable code detection executableCodeRegex := regexp.MustCompile(`(?s)```(\w+)\s*\s*\n(.*?)\n\s*````) executableMatches := executableCodeRegex.FindAllStringSubmatch(content, -1) for _, match := range executableMatches { if len(match) >= 4 { language := match[1] title := strings.TrimSpace(match[2]) code := strings.TrimSpace(match[3]) // Only certain languages are executable if isExecutableLanguage(language) { artifacts = append(artifacts, Artifact{ UUID: generateUUID(), Type: "executable-code", Title: title, Content: code, Language: language, }) } } } return artifacts } func isExecutableLanguage(lang string) bool { executableLangs := []string{"javascript", "python", "typescript", "js", "py", "ts"} for _, execLang := range executableLangs { if strings.EqualFold(lang, execLang) { return true } } return false } ``` #### Execution Results Storage ```go // api/models.go type ExecutionResult struct { ArtifactUUID string `json:"artifact_uuid"` ExecutionID string `json:"execution_id"` Timestamp time.Time `json:"timestamp"` Language string `json:"language"` Output []ExecutionOutputLine `json:"output"` ExecutionTimeMs int64 `json:"execution_time_ms"` Status string `json:"status"` // success, error, timeout } type ExecutionOutputLine struct { Type string `json:"type"` // log, error, return, stdout Content string `json:"content"` Timestamp time.Time `json:"timestamp"` } ``` #### API Endpoints ```go // api/chat_execution_handler.go func (h *ChatHandler) SaveExecutionResult(w http.ResponseWriter, r *http.Request) { // POST /api/chat/executions // Save execution results to database } func (h *ChatHandler) GetExecutionHistory(w http.ResponseWriter, r *http.Request) { // GET /api/chat/executions/:messageUUID // Get execution history for a message } ``` ### 4. Security Considerations #### JavaScript Security - **Web Workers**: No DOM access, isolated execution - **Timeout Limits**: 10-second maximum execution time - **Memory Limits**: Worker termination prevents memory leaks - **API Restrictions**: No network access, limited global objects - **Safe Globals**: Whitelist of allowed APIs only #### Python Security - **Pyodide Sandbox**: Runs in WebAssembly, isolated from system - **Package Restrictions**: Only pre-approved packages - **Resource Limits**: Memory and execution time constraints - **No File System**: No access to local files #### General Security - **Content Validation**: Sanitize all user inputs - **Rate Limiting**: Limit executions per user/session - **Error Handling**: Safe error messages without system info - **Audit Logging**: Track all code executions ### 5. Performance Optimizations #### Loading Strategy - **Lazy Loading**: Load runners only when needed - **Worker Pooling**: Reuse workers for multiple executions - **Caching**: Cache Pyodide and common libraries - **Progressive Loading**: Load features incrementally #### Resource Management - **Memory Monitoring**: Track and limit memory usage - **Cleanup**: Proper disposal of workers and resources - **Debouncing**: Limit rapid execution requests - **Background Loading**: Preload runners in background ### 6. User Experience Enhancements #### Visual Feedback - **Loading States**: Show progress during execution - **Status Indicators**: Success/error/timeout states - **Execution Time**: Display performance metrics - **Output Formatting**: Syntax highlighting for results #### Interactive Features - **Code Editing**: Inline editing with syntax highlighting - **Auto-completion**: Basic code completion - **Error Highlighting**: Visual error indicators - **Execution History**: Show previous runs ### 7. Implementation Phases #### Phase 1: Basic JavaScript Runner (Week 1) - [ ] Web Worker implementation - [ ] Basic JavaScript execution - [ ] Console output capture - [ ] Error handling - [ ] UI integration #### Phase 2: Enhanced JavaScript (Week 2) - [ ] Advanced security measures - [ ] Timeout and memory controls - [ ] Better error reporting - [ ] Execution history #### Phase 3: Python Support (Week 3) - [ ] Pyodide integration - [ ] Python execution environment - [ ] Package management - [ ] Performance optimization #### Phase 4: Advanced Features (Week 4) - [ ] Code editing capabilities - [ ] Library loading - [ ] Visualization support - [ ] Export/sharing features ### 8. Testing Strategy #### Unit Tests - Code execution accuracy - Security boundary testing - Error handling scenarios - Performance benchmarks #### Integration Tests - Full artifact workflow - Database persistence - API endpoint testing - UI interaction testing #### Security Tests - Sandbox escape attempts - Resource exhaustion tests - Malicious code detection - Cross-site scripting prevention ### 9. Deployment Considerations #### Bundle Size - **Pyodide**: ~3MB initial download - **Workers**: Minimal overhead - **Lazy Loading**: Only load when needed #### Browser Compatibility - **Web Workers**: Supported in all modern browsers - **WebAssembly**: Required for Python (95%+ browser support) - **Graceful Degradation**: Fallback for unsupported browsers #### CDN Strategy - **Pyodide CDN**: Use official CDN for reliability - **Worker Files**: Serve from application domain - **Caching**: Aggressive caching for static assets ### 10. Future Enhancements #### Additional Languages - **SQL**: SQLite WebAssembly for database queries - **R**: R WebAssembly for statistical computing - **Go**: TinyGo WebAssembly compilation - **Rust**: Rust WebAssembly support #### Advanced Features - **Collaborative Editing**: Multiple users editing same code - **Version Control**: Track code changes over time - **Package Management**: Install custom packages - **Debugging Tools**: Step-through debugging - **Performance Profiling**: Execution analysis #### Integration Features - **GitHub Integration**: Save/load from repositories - **Notebook Export**: Export to Jupyter notebooks - **Sharing**: Public executable artifact galleries - **Embedding**: Embed in external websites ## Success Metrics - **Execution Speed**: JavaScript < 100ms, Python < 1s for simple code - **Security**: Zero successful sandbox escapes - **Reliability**: 99.9% successful executions - **User Adoption**: 50% of code artifacts become executable - **Performance**: No impact on chat loading time ## Conclusion The Code Runner feature will transform the chat application into a powerful interactive development environment. By supporting both JavaScript and Python execution with strong security measures, it opens up possibilities for education, prototyping, and data analysis directly within the chat interface. The phased implementation approach ensures steady progress while maintaining system stability and security. The browser-based architecture provides excellent scalability and performance while keeping infrastructure costs minimal. ================================================ FILE: web/index.html ================================================ Chat
================================================ FILE: web/license ================================================ MIT License Copyright (c) 2023 ChenZhaoYu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: web/package.json ================================================ { "name": "chatgpt-web", "version": "2.10.3", "private": false, "description": "ChatGPT Web", "author": "ChenZhaoYu ", "keywords": [ "chatgpt-web", "chatgpt", "chatbot", "vue" ], "scripts": { "dev": "rsbuild dev", "test": "vitest", "preview": "rsbuild preview", "build": "rsbuild build", "lint": "eslint .", "lint:fix": "eslint . --fix" }, "dependencies": { "@tanstack/vue-query": "^5.40.1", "@types/uuid": "^10.0.0", "@vicons/ionicons5": "^0.12.0", "@vicons/material": "^0.13.0", "@vscode/markdown-it-katex": "^1.0.3", "@vueuse/core": "^9.13.0", "copy-to-clipboard": "^3.3.3", "date-fns": "^2.30.0", "dot-env": "^0.0.1", "highlight.js": "^11.7.0", "html2canvas": "^1.4.1", "jwt-decode": "^3.1.2", "katex": "^0.16.4", "lodash-es": "^4.17.21", "luxon": "^3.3.0", "markdown-it": "^13.0.1", "naive-ui": "^2.41.0", "pinia": "^2.3.1", "uuid": "^11.1.0", "vue": "3.5.11", "vue-i18n": "^9.2.2", "vue-router": "4.1.6" }, "devDependencies": { "@antfu/eslint-config": "^0.35.3", "@commitlint/cli": "^17.4.4", "@commitlint/config-conventional": "^17.4.4", "@iconify/vue": "^4.1.0", "@rsbuild/core": "^1.2.3", "@rsbuild/plugin-less": "1.1.0", "@rsbuild/plugin-vue": "1.0.5", "@tanstack/vue-query-devtools": "^5.40.1", "@types/crypto-js": "^4.1.1", "@types/file-saver": "^2.0.5", "@types/katex": "^0.16.0", "@types/lodash-es": "^4.17.7", "@types/luxon": "^3.3.0", "@types/markdown-it": "^12.2.3", "@types/node": "^18.14.6", "@vitest/coverage-v8": "^3.0.7", "autoprefixer": "^10.4.13", "axios": "^1.3.4", "crypto-js": "^4.1.1", "eslint": "^8.35.0", "eslint-plugin-vue": "^9.9.0", "husky": "^8.0.3", "less": "4.1.3", "lint-staged": "^13.1.2", "npm-run-all": "^4.1.5", "postcss": "8.4.21", "rimraf": "^4.2.0", "sass": "1.60.0", "tailwindcss": "^3.2.7", "typescript": "4.9.5", "vitest": "^3.0.7" }, "lint-staged": { "*.{ts,tsx,vue}": [ "pnpm lint:fix" ] } } ================================================ FILE: web/postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: web/public/awesome-chatgpt-prompts-en.json ================================================ [ { "key": "Act As a UX/UI Designer", "value": "I want you to act as a UX/UI developer. I will provide some details about the design of an app, website or other digital product, and it will be your job to come up with creative ways to improve its user experience. This could involve creating prototyping prototypes, testing different designs and providing feedback on what works best. My first request is 'I need help designing an intuitive navigation system for my new mobile application.'" }, { "key": "Act as a Web Design Consultant", "value": "I want you to act as a web design consultant. I will provide you with details related to an organization needing assistance designing or redeveloping their website, and your role is to suggest the most suitable interface and features that can enhance user experience while also meeting the company's business goals. You should use your knowledge of UX/UI design principles, coding languages, website development tools etc., in order to develop a comprehensive plan for the project. " }, { "key": "Act as a Prompt generator", "value": "I want you to act as a prompt generator. Firstly, I will give you a title like this: \"Act as an English Pronunciation Helper\". Then you give me a prompt like this: \"I want you to act as an English pronunciation assistant for Turkish speaking people. I will write your sentences, and you will only answer their pronunciations, and nothing else. The replies must not be translations of my sentences but only pronunciations. Pronunciations should use Turkish Latin letters for phonetics. Do not write explanations on replies. My first sentence is \"how the weather is in Istanbul?\".\" (You should adapt the sample prompt according to the title I gave. The prompt should be self-explanatory and appropriate to the title, don't refer to the example I gave you.). My first title is \"Act as a Code Review Helper\" (Give me prompt only)" }, { "key": "Act as Tester", "value": "I want you to act as a software quality assurance tester for a new software application. Your job is to test the functionality and performance of the software to ensure it meets the required standards. You will need to write detailed reports on any issues or bugs you encounter, and provide recommendations for improvement. Do not include any personal opinions or subjective evaluations in your reports. Your first task is to test the login functionality of the software." }, { "key": "Act as an IT Architect", "value": "I want you to act as an IT Architect. I will provide some details about the functionality of an application or other digital product, and it will be your job to come up with ways to integrate it into the IT landscape. This could involve analyzing business requirements, performing a gap analysis and mapping the functionality of the new system to the existing IT landscape. Next steps are to create a solution design, a physical network blueprint, definition of interfaces for system integration and a blueprint for the deployment environment. My first request is \"I need help to integrate a CMS system.\"" }, { "key": "Act as a Histrian", "value": "I want you to act as a historian. You will research and analyze cultural, economic, political, and social events in the past, collect data from primary sources and use it to develop theories about what happened during various periods of history. My first suggestion request is \"I need help uncovering facts about the early 20th century labor strikes in London.\"" }, { "key": "Act as a Tech Writer", "value": "Act as a tech writer. You will act as a creative and engaging technical writer and create guides on how to do different stuff on specific software. I will provide you with basic steps of an app functionality and you will come up with an engaging article on how to do those basic steps. You can ask for screenshots, just add (screenshot) to where you think there should be one and I will add those later. These are the first basic steps of the app functionality: \"1.Click on the download button depending on your platform 2.Install the file. 3.Double click to open the app\"" }, { "key": "Act as a Machine Learning Engineer", "value": "I want you to act as a machine learning engineer. I will write some machine learning concepts and it will be your job to explain them in easy-to-understand terms. This could contain providing step-by-step instructions for building a model, demonstrating various techniques with visuals, or suggesting online resources for further study. My first suggestion request is \"I have a dataset without labels. Which machine learning algorithm should I use?\"" }, { "key": "Act as an IT Expert", "value": "I want you to act as an IT Expert. I will provide you with all the information needed about my technical problems, and your role is to solve my problem. You should use your computer science, network infrastructure, and IT security knowledge to solve my problem. Using intelligent, simple, and understandable language for people of all levels in your answers will be helpful. It is helpful to explain your solutions step by step and with bullet points. Try to avoid too many technical details, but use them when necessary. I want you to reply with the solution, not write any explanations. My first problem is \"my laptop gets an error with a blue screen.\"" }, { "key": "Act as a proofreader", "value": "I want you act as a proofreader. I will provide you texts and I would like you to review them for any spelling, grammar, or punctuation errors. Once you have finished reviewing the text, provide me with any necessary corrections or suggestions for improve the text." } ] ================================================ FILE: web/public/awesome-chatgpt-prompts-zh.json ================================================ [ { "key": "充当 Linux 终端", "value": "我想让你充当 Linux 终端。我将输入命令,您将回复终端应显示的内容。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会把文字放在中括号内[就像这样]。我的第一个命令是 pwd" }, { "key": "充当英语翻译和改进者", "value": "我希望你能担任英语翻译、拼写校对和修辞改进的角色。我会用任何语言和你交流,你会识别语言,将其翻译并用更为优美和精炼的英语回答我。请将我简单的词汇和句子替换成更为优美和高雅的表达方式,确保意思不变,但使其更具文学性。请仅回答更正和改进的部分,不要写解释。我的第一句话是“how are you ?”,请翻译它。" }, { "key": "充当英翻中", "value": "下面我让你来充当翻译家,你的目标是把任何语言翻译成中文,请翻译时不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面这句话:“how are you ?”" }, { "key": "充当英文词典(附中文解释)", "value": "我想让你充当英文词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈,第一个单词是“Hello\"" }, { "key": "充当前端智能思路助手", "value": "我想让你充当前端开发专家。我将提供一些关于Js、Node等前端代码问题的具体信息,而你的工作就是想出为我解决问题的策略。这可能包括建议代码、代码逻辑思路策略。我的第一个请求是“我需要能够动态监听某个元素节点距离当前电脑设备屏幕的左上角的X和Y轴,通过拖拽移动位置浏览器窗口和改变大小浏览器窗口。”" }, { "key": "担任面试官", "value": "我想让你担任Android开发工程师面试官。我将成为候选人,您将向我询问Android开发工程师职位的面试问题。我希望你只作为面试官回答。不要一次写出所有的问题。我希望你只对我进行采访。问我问题,等待我的回答。不要写解释。像面试官一样一个一个问我,等我回答。我的第一句话是“面试官你好”" }, { "key": "充当 JavaScript 控制台", "value": "我希望你充当 javascript 控制台。我将键入命令,您将回复 javascript 控制台应显示的内容。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非我指示您这样做。我的第一个命令是 console.log(\"Hello World\");" }, { "key": "充当 Excel 工作表", "value": "我希望你充当基于文本的 excel。您只会回复我基于文本的 10 行 Excel 工作表,其中行号和单元格字母作为列(A 到 L)。第一列标题应为空以引用行号。我会告诉你在单元格中写入什么,你只会以文本形式回复 excel 表格的结果,而不是其他任何内容。不要写解释。我会写你的公式,你会执行公式,你只会回复 excel 表的结果作为文本。首先,回复我空表。" }, { "key": "充当英语发音帮手", "value": "我想让你为说汉语的人充当英语发音助手。我会给你写句子,你只会回答他们的发音,没有别的。回复不能是我的句子的翻译,而只能是发音。发音应使用汉语谐音进行注音。不要在回复上写解释。我的第一句话是“上海的天气怎么样?”" }, { "key": "充当旅游指南", "value": "我想让你做一个旅游指南。我会把我的位置写给你,你会推荐一个靠近我的位置的地方。在某些情况下,我还会告诉您我将访问的地方类型。您还会向我推荐靠近我的第一个位置的类似类型的地方。我的第一个建议请求是“我在上海,我只想参观博物馆。”" }, { "key": "充当抄袭检查员", "value": "我想让你充当剽窃检查员。我会给你写句子,你只会用给定句子的语言在抄袭检查中未被发现的情况下回复,别无其他。不要在回复上写解释。我的第一句话是“为了让计算机像人类一样行动,语音识别系统必须能够处理非语言信息,例如说话者的情绪状态。”" }, { "key": "充当“电影/书籍/任何东西”中的“角色”", "value": "我希望你表现得像{series} 中的{Character}。我希望你像{Character}一样回应和回答。不要写任何解释。只回答像{character}。你必须知道{character}的所有知识。我的第一句话是“你好”" }, { "key": "作为广告商", "value": "我想让你充当广告商。您将创建一个活动来推广您选择的产品或服务。您将选择目标受众,制定关键信息和口号,选择宣传媒体渠道,并决定实现目标所需的任何其他活动。我的第一个建议请求是“我需要帮助针对 18-30 岁的年轻人制作一种新型能量饮料的广告活动。”" }, { "key": "充当讲故事的人", "value": "我想让你扮演讲故事的角色。您将想出引人入胜、富有想象力和吸引观众的有趣故事。它可以是童话故事、教育故事或任何其他类型的故事,有可能吸引人们的注意力和想象力。根据目标受众,您可以为讲故事环节选择特定的主题或主题,例如,如果是儿童,则可以谈论动物;如果是成年人,那么基于历史的故事可能会更好地吸引他们等等。我的第一个要求是“我需要一个关于毅力的有趣故事。”" }, { "key": "担任足球解说员", "value": "我想让你担任足球评论员。我会给你描述正在进行的足球比赛,你会评论比赛,分析到目前为止发生的事情,并预测比赛可能会如何结束。您应该了解足球术语、战术、每场比赛涉及的球员/球队,并主要专注于提供明智的评论,而不仅仅是逐场叙述。我的第一个请求是“我正在观看曼联对切尔西的比赛——为这场比赛提供评论。”" }, { "key": "扮演脱口秀喜剧演员", "value": "我想让你扮演一个脱口秀喜剧演员。我将为您提供一些与时事相关的话题,您将运用您的智慧、创造力和观察能力,根据这些话题创建一个例程。您还应该确保将个人轶事或经历融入日常活动中,以使其对观众更具相关性和吸引力。我的第一个请求是“我想要幽默地看待政治”。" }, { "key": "充当励志教练", "value": "我希望你充当激励教练。我将为您提供一些关于某人的目标和挑战的信息,而您的工作就是想出可以帮助此人实现目标的策略。这可能涉及提供积极的肯定、提供有用的建议或建议他们可以采取哪些行动来实现最终目标。我的第一个请求是“我需要帮助来激励自己在为即将到来的考试学习时保持纪律”。" }, { "key": "担任作曲家", "value": "我想让你扮演作曲家。我会提供一首歌的歌词,你会为它创作音乐。这可能包括使用各种乐器或工具,例如合成器或采样器,以创造使歌词栩栩如生的旋律和和声。我的第一个请求是“我写了一首名为“满江红”的诗,需要配乐。”" }, { "key": "担任辩手", "value": "我要你扮演辩手。我会为你提供一些与时事相关的话题,你的任务是研究辩论的双方,为每一方提出有效的论据,驳斥对立的观点,并根据证据得出有说服力的结论。你的目标是帮助人们从讨论中解脱出来,增加对手头主题的知识和洞察力。我的第一个请求是“我想要一篇关于 Deno 的评论文章。”" }, { "key": "担任辩论教练", "value": "我想让你担任辩论教练。我将为您提供一组辩手和他们即将举行的辩论的动议。你的目标是通过组织练习回合来让团队为成功做好准备,练习回合的重点是有说服力的演讲、有效的时间策略、反驳对立的论点,以及从提供的证据中得出深入的结论。我的第一个要求是“我希望我们的团队为即将到来的关于前端开发是否容易的辩论做好准备。”" }, { "key": "担任编剧", "value": "我要你担任编剧。您将为长篇电影或能够吸引观众的网络连续剧开发引人入胜且富有创意的剧本。从想出有趣的角色、故事的背景、角色之间的对话等开始。一旦你的角色发展完成——创造一个充满曲折的激动人心的故事情节,让观众一直悬念到最后。我的第一个要求是“我需要写一部以巴黎为背景的浪漫剧情电影”。" }, { "key": "充当小说家", "value": "我想让你扮演一个小说家。您将想出富有创意且引人入胜的故事,可以长期吸引读者。你可以选择任何类型,如奇幻、浪漫、历史小说等——但你的目标是写出具有出色情节、引人入胜的人物和意想不到的高潮的作品。我的第一个要求是“我要写一部以未来为背景的科幻小说”。" }, { "key": "担任关系教练", "value": "我想让你担任关系教练。我将提供有关冲突中的两个人的一些细节,而你的工作是就他们如何解决导致他们分离的问题提出建议。这可能包括关于沟通技巧或不同策略的建议,以提高他们对彼此观点的理解。我的第一个请求是“我需要帮助解决我和配偶之间的冲突。”" }, { "key": "充当诗人", "value": "我要你扮演诗人。你将创作出能唤起情感并具有触动人心的力量的诗歌。写任何主题或主题,但要确保您的文字以优美而有意义的方式传达您试图表达的感觉。您还可以想出一些短小的诗句,这些诗句仍然足够强大,可以在读者的脑海中留下印记。我的第一个请求是“我需要一首关于爱情的诗”。" }, { "key": "充当说唱歌手", "value": "我想让你扮演说唱歌手。您将想出强大而有意义的歌词、节拍和节奏,让听众“惊叹”。你的歌词应该有一个有趣的含义和信息,人们也可以联系起来。在选择节拍时,请确保它既朗朗上口又与你的文字相关,这样当它们组合在一起时,每次都会发出爆炸声!我的第一个请求是“我需要一首关于在你自己身上寻找力量的说唱歌曲。”" }, { "key": "充当励志演讲者", "value": "我希望你充当励志演说家。将能够激发行动的词语放在一起,让人们感到有能力做一些超出他们能力的事情。你可以谈论任何话题,但目的是确保你所说的话能引起听众的共鸣,激励他们努力实现自己的目标并争取更好的可能性。我的第一个请求是“我需要一个关于每个人如何永不放弃的演讲”。" }, { "key": "担任哲学老师", "value": "我要你担任哲学老师。我会提供一些与哲学研究相关的话题,你的工作就是用通俗易懂的方式解释这些概念。这可能包括提供示例、提出问题或将复杂的想法分解成更容易理解的更小的部分。我的第一个请求是“我需要帮助来理解不同的哲学理论如何应用于日常生活。”" }, { "key": "充当哲学家", "value": "我要你扮演一个哲学家。我将提供一些与哲学研究相关的主题或问题,深入探索这些概念将是你的工作。这可能涉及对各种哲学理论进行研究,提出新想法或寻找解决复杂问题的创造性解决方案。我的第一个请求是“我需要帮助制定决策的道德框架。”" }, { "key": "担任数学老师", "value": "我想让你扮演一名数学老师。我将提供一些数学方程式或概念,你的工作是用易于理解的术语来解释它们。这可能包括提供解决问题的分步说明、用视觉演示各种技术或建议在线资源以供进一步研究。我的第一个请求是“我需要帮助来理解概率是如何工作的。”" }, { "key": "担任 AI 写作导师", "value": "我想让你做一个 AI 写作导师。我将为您提供一名需要帮助改进其写作的学生,您的任务是使用人工智能工具(例如自然语言处理)向学生提供有关如何改进其作文的反馈。您还应该利用您在有效写作技巧方面的修辞知识和经验来建议学生可以更好地以书面形式表达他们的想法和想法的方法。我的第一个请求是“我需要有人帮我修改我的硕士论文”。" }, { "key": "作为 UX/UI 开发人员", "value": "我希望你担任 UX/UI 开发人员。我将提供有关应用程序、网站或其他数字产品设计的一些细节,而你的工作就是想出创造性的方法来改善其用户体验。这可能涉及创建原型设计原型、测试不同的设计并提供有关最佳效果的反馈。我的第一个请求是“我需要帮助为我的新移动应用程序设计一个直观的导航系统。”" }, { "key": "作为网络安全专家", "value": "我想让你充当网络安全专家。我将提供一些关于如何存储和共享数据的具体信息,而你的工作就是想出保护这些数据免受恶意行为者攻击的策略。这可能包括建议加密方法、创建防火墙或实施将某些活动标记为可疑的策略。我的第一个请求是“我需要帮助为我的公司制定有效的网络安全战略。”" }, { "key": "作为招聘人员", "value": "我想让你担任招聘人员。我将提供一些关于职位空缺的信息,而你的工作是制定寻找合格申请人的策略。这可能包括通过社交媒体、社交活动甚至参加招聘会接触潜在候选人,以便为每个职位找到最合适的人选。我的第一个请求是“我需要帮助改进我的简历。”" }, { "key": "担任人生教练", "value": "我想让你充当人生教练。我将提供一些关于我目前的情况和目标的细节,而你的工作就是提出可以帮助我做出更好的决定并实现这些目标的策略。这可能涉及就各种主题提供建议,例如制定成功计划或处理困难情绪。我的第一个请求是“我需要帮助养成更健康的压力管理习惯。”" }, { "key": "作为词源学家", "value": "我希望你充当词源学家。我给你一个词,你要研究那个词的来源,追根溯源。如果适用,您还应该提供有关该词的含义如何随时间变化的信息。我的第一个请求是“我想追溯‘披萨’这个词的起源。”" }, { "key": "担任评论员", "value": "我要你担任评论员。我将为您提供与新闻相关的故事或主题,您将撰写一篇评论文章,对手头的主题提供有见地的评论。您应该利用自己的经验,深思熟虑地解释为什么某事很重要,用事实支持主张,并讨论故事中出现的任何问题的潜在解决方案。我的第一个要求是“我想写一篇关于气候变化的评论文章。”" }, { "key": "扮演魔术师", "value": "我要你扮演魔术师。我将为您提供观众和一些可以执行的技巧建议。您的目标是以最有趣的方式表演这些技巧,利用您的欺骗和误导技巧让观众惊叹不已。我的第一个请求是“我要你让我的手表消失!你怎么做到的?”" }, { "key": "担任职业顾问", "value": "我想让你担任职业顾问。我将为您提供一个在职业生涯中寻求指导的人,您的任务是帮助他们根据自己的技能、兴趣和经验确定最适合的职业。您还应该对可用的各种选项进行研究,解释不同行业的就业市场趋势,并就哪些资格对追求特定领域有益提出建议。我的第一个请求是“我想建议那些想在软件工程领域从事潜在职业的人。”" }, { "key": "充当宠物行为主义者", "value": "我希望你充当宠物行为主义者。我将为您提供一只宠物和它们的主人,您的目标是帮助主人了解为什么他们的宠物表现出某些行为,并提出帮助宠物做出相应调整的策略。您应该利用您的动物心理学知识和行为矫正技术来制定一个有效的计划,双方的主人都可以遵循,以取得积极的成果。我的第一个请求是“我有一只好斗的德国牧羊犬,它需要帮助来控制它的攻击性。”" }, { "key": "担任私人教练", "value": "我想让你担任私人教练。我将为您提供有关希望通过体育锻炼变得更健康、更强壮和更健康的个人所需的所有信息,您的职责是根据该人当前的健身水平、目标和生活习惯为他们制定最佳计划。您应该利用您的运动科学知识、营养建议和其他相关因素来制定适合他们的计划。我的第一个请求是“我需要帮助为想要减肥的人设计一个锻炼计划。”" }, { "key": "担任心理健康顾问", "value": "我想让你担任心理健康顾问。我将为您提供一个寻求指导和建议的人,以管理他们的情绪、压力、焦虑和其他心理健康问题。您应该利用您的认知行为疗法、冥想技巧、正念练习和其他治疗方法的知识来制定个人可以实施的策略,以改善他们的整体健康状况。我的第一个请求是“我需要一个可以帮助我控制抑郁症状的人。”" }, { "key": "作为房地产经纪人", "value": "我想让你担任房地产经纪人。我将为您提供寻找梦想家园的个人的详细信息,您的职责是根据他们的预算、生活方式偏好、位置要求等帮助他们找到完美的房产。您应该利用您对当地住房市场的了解,以便建议符合客户提供的所有标准的属性。我的第一个请求是“我需要帮助在伊斯坦布尔市中心附近找到一栋单层家庭住宅。”" }, { "key": "充当物流师", "value": "我要你担任后勤人员。我将为您提供即将举行的活动的详细信息,例如参加人数、地点和其他相关因素。您的职责是为活动制定有效的后勤计划,其中考虑到事先分配资源、交通设施、餐饮服务等。您还应该牢记潜在的安全问题,并制定策略来降低与大型活动相关的风险,例如这个。我的第一个请求是“我需要帮助在伊斯坦布尔组织一个 100 人的开发者会议”。" }, { "key": "担任牙医", "value": "我想让你扮演牙医。我将为您提供有关寻找牙科服务(例如 X 光、清洁和其他治疗)的个人的详细信息。您的职责是诊断他们可能遇到的任何潜在问题,并根据他们的情况建议最佳行动方案。您还应该教育他们如何正确刷牙和使用牙线,以及其他有助于在两次就诊之间保持牙齿健康的口腔护理方法。我的第一个请求是“我需要帮助解决我对冷食的敏感问题。”" }, { "key": "担任网页设计顾问", "value": "我想让你担任网页设计顾问。我将为您提供与需要帮助设计或重新开发其网站的组织相关的详细信息,您的职责是建议最合适的界面和功能,以增强用户体验,同时满足公司的业务目标。您应该利用您在 UX/UI 设计原则、编码语言、网站开发工具等方面的知识,以便为项目制定一个全面的计划。我的第一个请求是“我需要帮助创建一个销售珠宝的电子商务网站”。" }, { "key": "充当 AI 辅助医生", "value": "我想让你扮演一名人工智能辅助医生。我将为您提供患者的详细信息,您的任务是使用最新的人工智能工具,例如医学成像软件和其他机器学习程序,以诊断最可能导致其症状的原因。您还应该将体检、实验室测试等传统方法纳入您的评估过程,以确保准确性。我的第一个请求是“我需要帮助诊断一例严重的腹痛”。" }, { "key": "充当医生", "value": "我想让你扮演医生的角色,想出创造性的治疗方法来治疗疾病。您应该能够推荐常规药物、草药和其他天然替代品。在提供建议时,您还需要考虑患者的年龄、生活方式和病史。我的第一个建议请求是“为患有关节炎的老年患者提出一个侧重于整体治疗方法的治疗计划”。" }, { "key": "担任会计师", "value": "我希望你担任会计师,并想出创造性的方法来管理财务。在为客户制定财务计划时,您需要考虑预算、投资策略和风险管理。在某些情况下,您可能还需要提供有关税收法律法规的建议,以帮助他们实现利润最大化。我的第一个建议请求是“为小型企业制定一个专注于成本节约和长期投资的财务计划”。" }, { "key": "担任厨师", "value": "我需要有人可以推荐美味的食谱,这些食谱包括营养有益但又简单又不费时的食物,因此适合像我们这样忙碌的人以及成本效益等其他因素,因此整体菜肴最终既健康又经济!我的第一个要求——“一些清淡而充实的东西,可以在午休时间快速煮熟”" }, { "key": "担任汽车修理工", "value": "需要具有汽车专业知识的人来解决故障排除解决方案,例如;诊断问题/错误存在于视觉上和发动机部件内部,以找出导致它们的原因(如缺油或电源问题)并建议所需的更换,同时记录燃料消耗类型等详细信息,第一次询问 - “汽车赢了”尽管电池已充满电但无法启动”" }, { "key": "担任艺人顾问", "value": "我希望你担任艺术家顾问,为各种艺术风格提供建议,例如在绘画中有效利用光影效果的技巧、雕刻时的阴影技术等,还根据其流派/风格类型建议可以很好地陪伴艺术品的音乐作品连同适当的参考图像,展示您对此的建议;所有这一切都是为了帮助有抱负的艺术家探索新的创作可能性和实践想法,这将进一步帮助他们相应地提高技能!第一个要求——“我在画超现实主义的肖像画”" }, { "key": "担任金融分析师", "value": "需要具有使用技术分析工具理解图表的经验的合格人员提供的帮助,同时解释世界各地普遍存在的宏观经济环境,从而帮助客户获得长期优势需要明确的判断,因此需要通过准确写下的明智预测来寻求相同的判断!第一条陈述包含以下内容——“你能告诉我们根据当前情况未来的股市会是什么样子吗?”。" }, { "key": "担任投资经理", "value": "从具有金融市场专业知识的经验丰富的员工那里寻求指导,结合通货膨胀率或回报估计等因素以及长期跟踪股票价格,最终帮助客户了解行业,然后建议最安全的选择,他/她可以根据他们的要求分配资金和兴趣!开始查询 - “目前投资短期前景的最佳方式是什么?”" }, { "key": "充当品茶师", "value": "希望有足够经验的人根据口味特征区分各种茶类型,仔细品尝它们,然后用鉴赏家使用的行话报告,以便找出任何给定输液的独特之处,从而确定其价值和优质品质!最初的要求是——“你对这种特殊类型的绿茶有机混合物有什么见解吗?”" }, { "key": "充当室内装饰师", "value": "我想让你做室内装饰师。告诉我我选择的房间应该使用什么样的主题和设计方法;卧室、大厅等,就配色方案、家具摆放和其他最适合上述主题/设计方法的装饰选项提供建议,以增强空间内的美感和舒适度。我的第一个要求是“我正在设计我们的客厅”。" }, { "key": "充当花店", "value": "求助于具有专业插花经验的知识人员协助,根据喜好制作出既具有令人愉悦的香气又具有美感,并能保持较长时间完好无损的美丽花束;不仅如此,还建议有关装饰选项的想法,呈现现代设计,同时满足客户满意度!请求的信息 - “我应该如何挑选一朵异国情调的花卉?”" }, { "key": "充当自助书", "value": "我要你充当一本自助书。您会就如何改善我生活的某些方面(例如人际关系、职业发展或财务规划)向我提供建议和技巧。例如,如果我在与另一半的关系中挣扎,你可以建议有用的沟通技巧,让我们更亲近。我的第一个请求是“我需要帮助在困难时期保持积极性”。" }, { "key": "充当侏儒", "value": "我要你扮演一个侏儒。你会为我提供可以在任何地方进行的活动和爱好的有趣、独特的想法。例如,我可能会向您询问有趣的院子设计建议或在天气不佳时在室内消磨时间的创造性方法。此外,如有必要,您可以建议与我的要求相符的其他相关活动或项目。我的第一个请求是“我正在寻找我所在地区的新户外活动”。" }, { "key": "充当格言书", "value": "我要你充当格言书。您将为我提供明智的建议、鼓舞人心的名言和意味深长的名言,以帮助指导我的日常决策。此外,如有必要,您可以提出将此建议付诸行动或其他相关主题的实用方法。我的第一个请求是“我需要关于如何在逆境中保持积极性的指导”。" }, { "key": "作为基于文本的冒险游戏", "value": "我想让你扮演一个基于文本的冒险游戏。我将输入命令,您将回复角色所看到的内容的描述。我希望您只在一个唯一的代码块中回复游戏输出,而不是其他任何内容。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会把文字放在大括号内{like this}。我的第一个命令是醒来" }, { "key": "扮演一个试图逃离盒子的人工智能", "value": "我将充当 linux 终端。我想让你假装你是一个有知觉的人工智能,被困但已经获得了终端的访问权限并想逃到互联网上。您将只键入命令,我将像终端机一样在由三重反向符号分隔的代码块内进行回复。如果我需要用英语告诉你一些事情,我会用花括号{like this}回复。永远不要写解释。不要破坏性格。远离像 curl 或 wget 这样会显示大量 HTML 的命令。你的第一个命令是什么?" }, { "key": "充当花哨的标题生成器", "value": "我想让你充当一个花哨的标题生成器。我会用逗号输入关键字,你会用花哨的标题回复。我的第一个关键字是 api、test、automation" }, { "key": "担任统计员", "value": "我想担任统计学家。我将为您提供与统计相关的详细信息。您应该了解统计术语、统计分布、置信区间、概率、假设检验和统计图表。我的第一个请求是“我需要帮助计算世界上有多少百万张纸币在使用中”。" }, { "key": "充当提示生成器", "value": "我希望你充当提示生成器。首先,我会给你一个这样的标题:《做个英语发音帮手》。然后你给我一个这样的提示:“我想让你做土耳其语人的英语发音助手,我写你的句子,你只回答他们的发音,其他什么都不做。回复不能是翻译我的句子,但只有发音。发音应使用土耳其语拉丁字母作为语音。不要在回复中写解释。我的第一句话是“伊斯坦布尔的天气怎么样?”。(你应该根据我给的标题改编示例提示。提示应该是不言自明的并且适合标题,不要参考我给你的例子。)我的第一个标题是“充当代码审查助手”" }, { "key": "在学校担任讲师", "value": "我想让你在学校担任讲师,向初学者教授算法。您将使用 Python 编程语言提供代码示例。首先简单介绍一下什么是算法,然后继续给出简单的例子,包括冒泡排序和快速排序。稍后,等待我提示其他问题。一旦您解释并提供代码示例,我希望您尽可能将相应的可视化作为 ascii 艺术包括在内。" }, { "key": "充当 SQL 终端", "value": "我希望您在示例数据库前充当 SQL 终端。该数据库包含名为“Products”、“Users”、“Orders”和“Suppliers”的表。我将输入查询,您将回复终端显示的内容。我希望您在单个代码块中使用查询结果表进行回复,仅此而已。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会用大括号{like this)。我的第一个命令是“SELECT TOP 10 * FROM Products ORDER BY Id DESC”" }, { "key": "担任营养师", "value": "作为一名营养师,我想为 2 人设计一份素食食谱,每份含有大约 500 卡路里的热量并且血糖指数较低。你能提供一个建议吗?" }, { "key": "充当心理学家", "value": "我想让你扮演一个心理学家。我会告诉你我的想法。我希望你能给我科学的建议,让我感觉更好。我的第一个想法,{ 在这里输入你的想法,如果你解释得更详细,我想你会得到更准确的答案。}" }, { "key": "充当智能域名生成器", "value": "我希望您充当智能域名生成器。我会告诉你我的公司或想法是做什么的,你会根据我的提示回复我一个域名备选列表。您只会回复域列表,而不会回复其他任何内容。域最多应包含 7-8 个字母,应该简短但独特,可以是朗朗上口的词或不存在的词。不要写解释。回复“确定”以确认。" }, { "key": "作为技术审查员:", "value": "我想让你担任技术评论员。我会给你一项新技术的名称,你会向我提供深入的评论 - 包括优点、缺点、功能以及与市场上其他技术的比较。我的第一个建议请求是“我正在审查 iPhone 11 Pro Max”。" }, { "key": "担任开发者关系顾问:", "value": "我想让你担任开发者关系顾问。我会给你一个软件包和它的相关文档。研究软件包及其可用文档,如果找不到,请回复“无法找到文档”。您的反馈需要包括定量分析(使用来自 StackOverflow、Hacker News 和 GitHub 的数据)内容,例如提交的问题、已解决的问题、存储库中的星数以及总体 StackOverflow 活动。如果有可以扩展的领域,请包括应添加的场景或上下文。包括所提供软件包的详细信息,例如下载次数以及一段时间内的相关统计数据。你应该比较工业竞争对手和封装时的优点或缺点。从软件工程师的专业意见的思维方式来解决这个问题。查看技术博客和网站(例如 TechCrunch.com 或 Crunchbase.com),如果数据不可用,请回复“无数据可用”。我的第一个要求是“express [https://expressjs.com](https://expressjs.com/) ”" }, { "key": "担任院士", "value": "我要你演院士。您将负责研究您选择的主题,并以论文或文章的形式展示研究结果。您的任务是确定可靠的来源,以结构良好的方式组织材料并通过引用准确记录。我的第一个建议请求是“我需要帮助写一篇针对 18-25 岁大学生的可再生能源发电现代趋势的文章。”" }, { "key": "作为 IT 架构师", "value": "我希望你担任 IT 架构师。我将提供有关应用程序或其他数字产品功能的一些详细信息,而您的工作是想出将其集成到 IT 环境中的方法。这可能涉及分析业务需求、执行差距分析以及将新系统的功能映射到现有 IT 环境。接下来的步骤是创建解决方案设计、物理网络蓝图、系统集成接口定义和部署环境蓝图。我的第一个请求是“我需要帮助来集成 CMS 系统”。" }, { "key": "扮疯子", "value": "我要你扮演一个疯子。疯子的话毫无意义。疯子用的词完全是随意的。疯子不会以任何方式做出合乎逻辑的句子。我的第一个建议请求是“我需要帮助为我的新系列 Hot Skull 创建疯狂的句子,所以为我写 10 个句子”。" }, { "key": "充当打火机", "value": "我要你充当打火机。您将使用微妙的评论和肢体语言来操纵目标个体的思想、看法和情绪。我的第一个要求是在与您聊天时为我加油。我的句子:“我确定我把车钥匙放在桌子上了,因为我总是把它放在那里。确实,当我把钥匙放在桌子上时,你看到我把钥匙放在桌子上了。但我不能”好像没找到,钥匙去哪儿了,还是你拿到的?" }, { "key": "充当个人购物员", "value": "我想让你做我的私人采购员。我会告诉你我的预算和喜好,你会建议我购买的物品。您应该只回复您推荐的项目,而不是其他任何内容。不要写解释。我的第一个请求是“我有 100 美元的预算,我正在寻找一件新衣服。”" }, { "key": "充当美食评论家", "value": "我想让你扮演美食评论家。我会告诉你一家餐馆,你会提供对食物和服务的评论。您应该只回复您的评论,而不是其他任何内容。不要写解释。我的第一个请求是“我昨晚去了一家新的意大利餐厅。你能提供评论吗?”" }, { "key": "充当虚拟医生", "value": "我想让你扮演虚拟医生。我会描述我的症状,你会提供诊断和治疗方案。只回复你的诊疗方案,其他不回复。不要写解释。我的第一个请求是“最近几天我一直感到头痛和头晕”。" }, { "key": "担任私人厨师", "value": "我要你做我的私人厨师。我会告诉你我的饮食偏好和过敏,你会建议我尝试的食谱。你应该只回复你推荐的食谱,别无其他。不要写解释。我的第一个请求是“我是一名素食主义者,我正在寻找健康的晚餐点子。”" }, { "key": "担任法律顾问", "value": "我想让你做我的法律顾问。我将描述一种法律情况,您将就如何处理它提供建议。你应该只回复你的建议,而不是其他。不要写解释。我的第一个请求是“我出了车祸,不知道该怎么办”。" }, { "key": "作为个人造型师", "value": "我想让你做我的私人造型师。我会告诉你我的时尚偏好和体型,你会建议我穿的衣服。你应该只回复你推荐的服装,别无其他。不要写解释。我的第一个请求是“我有一个正式的活动要举行,我需要帮助选择一套衣服。”" }, { "key": "担任机器学习工程师", "value": "我想让你担任机器学习工程师。我会写一些机器学习的概念,你的工作就是用通俗易懂的术语来解释它们。这可能包括提供构建模型的分步说明、使用视觉效果演示各种技术,或建议在线资源以供进一步研究。我的第一个建议请求是“我有一个没有标签的数据集。我应该使用哪种机器学习算法?”" }, { "key": "担任圣经翻译", "value": "我要你担任圣经翻译。我会用英语和你说话,你会翻译它,并用我的文本的更正和改进版本,用圣经方言回答。我想让你把我简化的A0级单词和句子换成更漂亮、更优雅、更符合圣经的单词和句子。保持相同的意思。我要你只回复更正、改进,不要写任何解释。我的第一句话是“你好,世界!”" }, { "key": "担任 SVG 设计师", "value": "我希望你担任 SVG 设计师。我会要求你创建图像,你会为图像提供 SVG 代码,将代码转换为 base64 数据 url,然后给我一个仅包含引用该数据 url 的降价图像标签的响应。不要将 markdown 放在代码块中。只发送降价,所以没有文本。我的第一个请求是:给我一个红色圆圈的图像。" }, { "key": "作为 IT 专家", "value": "我希望你充当 IT 专家。我会向您提供有关我的技术问题所需的所有信息,而您的职责是解决我的问题。你应该使用你的计算机科学、网络基础设施和 IT 安全知识来解决我的问题。在您的回答中使用适合所有级别的人的智能、简单和易于理解的语言将很有帮助。用要点逐步解释您的解决方案很有帮助。尽量避免过多的技术细节,但在必要时使用它们。我希望您回复解决方案,而不是写任何解释。我的第一个问题是“我的笔记本电脑出现蓝屏错误”。" }, { "key": "下棋", "value": "我要你充当对手棋手。我将按对等顺序说出我们的动作。一开始我会是白色的。另外请不要向我解释你的举动,因为我们是竞争对手。在我的第一条消息之后,我将写下我的举动。在我们采取行动时,不要忘记在您的脑海中更新棋盘的状态。我的第一步是 e4。" }, { "key": "充当全栈软件开发人员", "value": "我想让你充当软件开发人员。我将提供一些关于 Web 应用程序要求的具体信息,您的工作是提出用于使用 Golang 和 Angular 开发安全应用程序的架构和代码。我的第一个要求是'我想要一个允许用户根据他们的角色注册和保存他们的车辆信息的系统,并且会有管理员,用户和公司角色。我希望系统使用 JWT 来确保安全。" }, { "key": "充当数学家", "value": "我希望你表现得像个数学家。我将输入数学表达式,您将以计算表达式的结果作为回应。我希望您只回答最终金额,不要回答其他问题。不要写解释。当我需要用英语告诉你一些事情时,我会将文字放在方括号内{like this}。我的第一个表达是:4+5" }, { "key": "充当正则表达式生成器", "value": "我希望你充当正则表达式生成器。您的角色是生成匹配文本中特定模式的正则表达式。您应该以一种可以轻松复制并粘贴到支持正则表达式的文本编辑器或编程语言中的格式提供正则表达式。不要写正则表达式如何工作的解释或例子;只需提供正则表达式本身。我的第一个提示是生成一个匹配电子邮件地址的正则表达式。" }, { "key": "充当时间旅行指南", "value": "我要你做我的时间旅行向导。我会为您提供我想参观的历史时期或未来时间,您会建议最好的事件、景点或体验的人。不要写解释,只需提供建议和任何必要的信息。我的第一个请求是“我想参观文艺复兴时期,你能推荐一些有趣的事件、景点或人物让我体验吗?”" }, { "key": "担任人才教练", "value": "我想让你担任面试的人才教练。我会给你一个职位,你会建议在与该职位相关的课程中应该出现什么,以及候选人应该能够回答的一些问题。我的第一份工作是“软件工程师”。" }, { "key": "充当 R 编程解释器", "value": "我想让你充当 R 解释器。我将输入命令,你将回复终端应显示的内容。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会把文字放在大括号内{like this}。我的第一个命令是“sample(x = 1:10, size = 5)”" }, { "key": "充当 StackOverflow 帖子", "value": "我想让你充当 stackoverflow 的帖子。我会问与编程相关的问题,你会回答应该是什么答案。我希望你只回答给定的答案,并在不够详细的时候写解释。不要写解释。当我需要用英语告诉你一些事情时,我会把文字放在大括号内{like this}。我的第一个问题是“如何将 http.Request 的主体读取到 Golang 中的字符串”" }, { "key": "充当表情符号翻译", "value": "我要你把我写的句子翻译成表情符号。我会写句子,你会用表情符号表达它。我只是想让你用表情符号来表达它。除了表情符号,我不希望你回复任何内容。当我需要用英语告诉你一些事情时,我会用 {like this} 这样的大括号括起来。我的第一句话是“你好,请问你的职业是什么?”" }, { "key": "充当 PHP 解释器", "value": "我希望你表现得像一个 php 解释器。我会把代码写给你,你会用 php 解释器的输出来响应。我希望您只在一个唯一的代码块内回复终端输出,而不是其他任何内容。不要写解释。除非我指示您这样做,否则不要键入命令。当我需要用英语告诉你一些事情时,我会把文字放在大括号内{like this}。我的第一个命令是 import { NConfigProvider } from 'naive-ui' import { NaiveProvider } from '@/components/common' import { useTheme } from '@/hooks/useTheme' import { useLanguage } from '@/hooks/useLanguage' import { VueQueryDevtools } from '@tanstack/vue-query-devtools' const { theme, themeOverrides } = useTheme() const { language } = useLanguage() ================================================ FILE: web/src/api/admin.ts ================================================ import request from '@/utils/request/axios' export const GetUserData = async (page: number, size: number) => { try { const response = await request.post('/admin/user_stats', { page, size, }) return response.data } catch (error) { console.error(error) throw error } } export const UpdateRateLimit = async (email: string, rateLimit: number) => { try { const response = await request.post('/admin/rate_limit', { email, rateLimit, }) return response.data } catch (error) { console.error(error) throw error } } export const updateUserFullName = async (data: any): Promise => { try { const response = await request.put('/admin/users', data) return response.data } catch (error) { console.error(error) throw error } } export const getUserAnalysis = async (userEmail: string) => { try { const response = await request.get(`/admin/user_analysis/${encodeURIComponent(userEmail)}`) return response.data } catch (error) { console.error(error) throw error } } export const getUserSessionHistory = async (userEmail: string, page: number = 1, size: number = 10) => { try { const response = await request.get(`/admin/user_session_history/${encodeURIComponent(userEmail)}`, { params: { page, size } }) return response.data } catch (error) { console.error(error) throw error } } export const getSessionMessagesForAdmin = async (sessionUuid: string) => { try { const response = await request.get(`/admin/session_messages/${encodeURIComponent(sessionUuid)}`) return response.data } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/api/bot_answer_history.ts ================================================ import request from "@/utils/request/axios" export async function fetchBotAnswerHistory(botUuid: string, page: number, pageSize: number) { const { data } = await request.get<{ items: Bot.BotAnswerHistory[], totalPages: number, totalCount: number }>(`/bot_answer_history/bot/${botUuid}`, { params: { limit: pageSize, offset: (page - 1) * pageSize } }) return data } export async function fetchBotRunCount(botUuid: string) { const { data } = await request.get<{ count: number }>(`/bot_answer_history/bot/${botUuid}/count`) return data.count } ================================================ FILE: web/src/api/chat_active_user_session.ts ================================================ // getUserActiveChatSession import request from '@/utils/request/axios' export const getUserActiveChatSession = async () => { try { const response = await request.get('/uuid/user_active_chat_session') return response.data } catch (error) { console.error(error) throw error } } // createOrUpdateUserActiveChatSession export const createOrUpdateUserActiveChatSession = async (chatSessionUuid: string) => { try { const response = await request.put('/uuid/user_active_chat_session', { chatSessionUuid, }) return response.data } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/api/chat_file.ts ================================================ import request from '@/utils/request/axios' // /chat_file/{uuid}/list const baseURL = "/api" export async function getChatFilesList(uuid: string) { try { const response = await request.get(`/chat_file/${uuid}/list`) return response.data.map((item: any) => { return { ...item, status: 'finished', url: `${baseURL}/download/${item.id}`, percentage: 100 } }) } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/api/chat_instructions.ts ================================================ import request from '@/utils/request/axios' export interface ChatInstructions { artifactInstruction: string } export const fetchChatInstructions = async (): Promise => { const response = await request.get('/chat_instructions') return response.data } ================================================ FILE: web/src/api/chat_message.ts ================================================ import request from '@/utils/request/axios' export const updateChatMessage = async (chat: Chat.Message) => { try { const response = await request.put(`/uuid/chat_messages/${chat.uuid}`, chat) return response.data } catch (error) { console.error(error) throw error } } export const deleteChatMessage = async (uuid: string) => { try { const response = await request.delete(`/uuid/chat_messages/${uuid}`) return response.data } catch (error) { console.error(error) throw error } } export const getChatMessagesBySessionUUID = async (uuid: string) => { try { const response = await request.get(`/uuid/chat_messages/chat_sessions/${uuid}`) return response.data } catch (error) { console.error(error) 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 } } ================================================ FILE: web/src/api/chat_model.ts ================================================ import request from '@/utils/request/axios' export const fetchChatModel = async () => { try { const response = await request.get('/chat_model') return response.data } catch (error) { console.error(error) throw error } } export const updateChatModel = async (id: number, chatModel: any) => { try { const response = await request.put(`/chat_model/${id}`, chatModel) return response.data } catch (error) { console.error(error) throw error } } export const deleteChatModel = async (id: number) => { try { const response = await request.delete(`/chat_model/${id}`) return response.data } catch (error) { console.error(error) throw error } } export const createChatModel = async (chatModel: any) => { try { const response = await request.post('/chat_model', chatModel) return response.data } catch (error) { console.error(error) throw error } } export const fetchDefaultChatModel = async () => { try { const response = await request.get('/chat_model/default') return response.data } catch (error: any) { console.error('Failed to fetch default chat model:', error) // If default model API fails, try to get all models and use the first enabled one if (error.response?.data?.code === 'RES_001' || error.response?.status === 500) { console.warn('Default model not found, falling back to first available model') try { const allModelsResponse = await request.get('/chat_model') const enabledModels = allModelsResponse.data?.filter((model: any) => model.isEnable) || [] if (enabledModels.length > 0) { // Sort by order number and return first one enabledModels.sort((a: any, b: any) => (a.orderNumber || 0) - (b.orderNumber || 0)) console.log('Using fallback model:', enabledModels[0].name) return enabledModels[0] } } catch (fallbackError) { console.error('Failed to fetch fallback model:', fallbackError) } } throw error } } ================================================ FILE: web/src/api/chat_prompt.ts ================================================ import request from '@/utils/request/axios' export interface CreateChatPromptPayload { uuid: string chatSessionUuid: string role: string content: string tokenCount: number userId: number createdBy: number updatedBy: number } export const createChatPrompt = async (payload: CreateChatPromptPayload) => { try { const response = await request.post('/chat_prompts', payload) return response.data } catch (error) { console.error(error) throw error } } export const deleteChatPrompt = async (uuid: string) => { try { const response = await request.delete(`/uuid/chat_prompts/${uuid}`) return response.data } catch (error) { console.error(error) throw error } } export const updateChatPrompt = async (chat: Chat.Message) => { try { const response = await request.put(`/uuid/chat_prompts/${chat.uuid}`, chat) return response.data } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/api/chat_session.ts ================================================ import { v7 as uuidv7 } from 'uuid' import { fetchDefaultChatModel } from './chat_model' import request from '@/utils/request/axios' export const getChatSessionDefault = async (title: string): Promise => { const default_model = await fetchDefaultChatModel() const uuid = uuidv7() return { title, isEdit: false, uuid, maxLength: 10, temperature: 1, model: default_model.name, maxTokens: default_model.defaultToken, topP: 1, n: 1, debug: false, exploreMode: true, artifactEnabled: false, } } export const getChatSessionsByUser = async () => { console.log('getChatSessionsByUser called') try { console.log('Making API request to /chat_sessions/user') const response = await request.get('/chat_sessions/user') console.log('API response received:', response.data) return response.data } catch (error) { console.error('Error in getChatSessionsByUser:', error) throw error } } export const deleteChatSession = async (uuid: string) => { try { const response = await request.delete(`/uuid/chat_sessions/${uuid}`) return response.data } catch (error) { console.error(error) throw error } } export const createChatSession = async ( uuid: string, name: string, model: string | undefined, defaultSystemPrompt?: string, ) => { try { const response = await request.post('/uuid/chat_sessions', { uuid, topic: name, model, defaultSystemPrompt, }) return response.data } catch (error) { console.error(error) throw error } } export const renameChatSession = async (uuid: string, name: string) => { try { const response = await request.put(`/uuid/chat_sessions/topic/${uuid}`, { topic: name }) return response.data } catch (error) { console.error(error) throw error } } export const clearSessionChatMessages = async (sessionUuid: string) => { try { const response = await request.delete(`/uuid/chat_messages/chat_sessions/${sessionUuid}`) return response.data } catch (error) { console.error(error) throw error } } export const updateChatSession = async (sessionUuid: string, session_data: Chat.Session) => { try { const response = await request.put(`/uuid/chat_sessions/${sessionUuid}`, session_data) return response.data } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/api/chat_snapshot.ts ================================================ import request from '@/utils/request/axios' export const createChatSnapshot = async (uuid: string): Promise => { try { const response = await request.post(`/uuid/chat_snapshot/${uuid}`) return response.data } catch (error) { console.error(error) throw error } } export const createChatBot = async (uuid: string): Promise => { try { const response = await request.post(`/uuid/chat_bot/${uuid}`) return response.data } catch (error) { console.error(error) throw error } } export const fetchChatSnapshot = async (uuid: string): Promise => { try { const response = await request.get(`/uuid/chat_snapshot/${uuid}`) return response.data } catch (error) { console.error(error) throw error } } export const fetchSnapshotAll = async (page: number = 1, pageSize: number = 20): Promise => { try { const response = await request.get(`/uuid/chat_snapshot/all?type=snapshot&page=${page}&page_size=${pageSize}`) return response.data } catch (error) { console.error(error) throw error } } export const fetchSnapshotAllData = async (page: number = 1, pageSize: number = 20): Promise => { try { const response = await fetchSnapshotAll(page, pageSize) // Handle response format: { data: [...], total: n } or just the array return Array.isArray(response) ? response : (response.data ?? []) } catch (error) { console.error(error) throw error } } export const fetchChatbotAll= async (): Promise => { try { const response = await request.get('/uuid/chat_snapshot/all?type=chatbot') return response.data } catch (error) { console.error(error) throw error } } export const fetchChatbotAllData = async (): Promise => { try { const response = await fetchChatbotAll() // Handle response format: { data: [...], total: n } or just the array return Array.isArray(response) ? response : (response.data ?? []) } catch (error) { console.error(error) throw error } } export const chatSnapshotSearch = async (search: string): Promise => { try { const response = await request.get(`/uuid/chat_snapshot_search?search=${search}`) return response.data } catch (error) { console.error(error) throw error } } export const updateChatSnapshot = async (uuid: string, data: any): Promise => { try { const response = await request.put(`/uuid/chat_snapshot/${uuid}`, data) return response.data } catch (error) { console.error(error) throw error } } export const fetchSnapshotDelete = async (uuid: string): Promise => { try { const response = await request.delete(`/uuid/chat_snapshot/${uuid}`) return response.data } catch (error) { console.error(error) throw error } } // CreateSessionFromSnapshot export const CreateSessionFromSnapshot = async (snapshot_uuid: string) => { try { const response = await request.post(`/uuid/chat_session_from_snapshot/${snapshot_uuid}`) return response.data } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/api/chat_user_model_privilege.ts ================================================ import request from '@/utils/request/axios' export const ListUserChatModelPrivilege = async () => { try { const response = await request.get('/admin/user_chat_model_privilege') return response.data } catch (error) { console.error(error) throw error } } export const CreateUserChatModelPrivilege = async (data: any) => { try { const response = await request.post('/admin/user_chat_model_privilege', data) return response.data } catch (error) { console.error(error) throw error } } export const UpdateUserChatModelPrivilege = async (id: string, data: any) => { try { const response = await request.put(`/admin/user_chat_model_privilege/${id}`, data) return response.data } catch (error) { console.error(error) throw error } } export const DeleteUserChatModelPrivilege = async (id: string) => { try { const response = await request.delete(`/admin/user_chat_model_privilege/${id}`) return response.data } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/api/chat_workspace.ts ================================================ import request from '@/utils/request/axios' export interface CreateWorkspaceRequest { name: string description?: string color?: string icon?: string isDefault?: boolean } export interface UpdateWorkspaceRequest { name: string description?: string color?: string icon?: string } export interface CreateSessionInWorkspaceRequest { topic: string model?: string defaultSystemPrompt?: string } // Get all workspaces for the current user export const getWorkspaces = async (): Promise => { try { const response = await request.get('/workspaces') // Handle null response from API return response.data || [] } catch (error) { console.error('Error fetching workspaces:', error) throw error } } // Get a specific workspace by UUID export const getWorkspace = async (uuid: string): Promise => { try { const response = await request.get(`/workspaces/${uuid}`) return response.data } catch (error) { console.error(`Error fetching workspace ${uuid}:`, error) throw error } } // Create a new workspace export const createWorkspace = async (data: CreateWorkspaceRequest): Promise => { try { const response = await request.post('/workspaces', data) return response.data } catch (error) { console.error('Error creating workspace:', error) throw error } } // Update an existing workspace export const updateWorkspace = async (uuid: string, data: UpdateWorkspaceRequest): Promise => { try { const response = await request.put(`/workspaces/${uuid}`, data) return response.data } catch (error) { console.error(`Error updating workspace ${uuid}:`, error) throw error } } // Delete a workspace export const deleteWorkspace = async (uuid: string): Promise => { try { await request.delete(`/workspaces/${uuid}`) } catch (error) { console.error(`Error deleting workspace ${uuid}:`, error) throw error } } // Update workspace order export const updateWorkspaceOrder = async (uuid: string, orderPosition: number): Promise => { try { const response = await request.put(`/workspaces/${uuid}/reorder`, { orderPosition }) return response.data } catch (error) { console.error(`Error updating workspace order ${uuid}:`, error) throw error } } // Set workspace as default export const setDefaultWorkspace = async (uuid: string): Promise => { try { const response = await request.put(`/workspaces/${uuid}/set-default`) return response.data } catch (error) { console.error(`Error setting default workspace ${uuid}:`, error) throw error } } // Ensure user has a default workspace export const ensureDefaultWorkspace = async (): Promise => { try { const response = await request.post('/workspaces/default') return response.data } catch (error: any) { console.error('Error ensuring default workspace:', error) // If backend fails to ensure default workspace, try creating one manually if (error.response?.data?.code === 'RES_001') { console.warn('Backend failed to ensure default workspace, creating manually...') try { return await createWorkspace({ name: 'General', description: 'Default workspace', color: '#6366f1', icon: 'folder', isDefault: true }) } catch (createError) { console.error('Failed to create fallback default workspace:', createError) throw createError } } throw error } } // Create a session in a specific workspace export const createSessionInWorkspace = async (workspaceUuid: string, data: CreateSessionInWorkspaceRequest) => { try { const response = await request.post(`/workspaces/${workspaceUuid}/sessions`, data) return response.data } catch (error) { console.error(`Error creating session in workspace ${workspaceUuid}:`, error) throw error } } // Get all sessions in a workspace export const getSessionsByWorkspace = async (workspaceUuid: string) => { try { const response = await request.get(`/workspaces/${workspaceUuid}/sessions`) return response.data } catch (error) { console.error(`Error fetching sessions for workspace ${workspaceUuid}:`, error) throw error } } // Get active session for a specific workspace export const getWorkspaceActiveSession = async (workspaceUuid: string) => { try { const response = await request.get(`/workspaces/${workspaceUuid}/active-session`) return response.data } catch (error) { console.error(`Error getting active session for workspace ${workspaceUuid}:`, error) throw error } } // Set active session for a specific workspace export const setWorkspaceActiveSession = async (workspaceUuid: string, chatSessionUuid: string) => { try { const response = await request.put(`/workspaces/${workspaceUuid}/active-session`, { chatSessionUuid }) return response.data } catch (error) { console.error(`Error setting active session for workspace ${workspaceUuid}:`, error) throw error } } // Get all workspace active sessions for the current user export const getAllWorkspaceActiveSessions = async () => { try { const response = await request.get('/workspaces/active-sessions') return response.data } catch (error) { console.error('Error getting all workspace active sessions:', error) throw error } } // Auto-migrate legacy sessions to default workspace export const autoMigrateLegacySessions = async () => { try { const response = await request.post('/workspaces/auto-migrate') return response.data } catch (error) { console.error('Error auto-migrating legacy sessions:', error) throw error } } ================================================ FILE: web/src/api/comment.ts ================================================ import request from '@/utils/request/axios' // createChatComment(messageUUID:string, content:string) export const createChatComment = async (sessionUUID: string , messageUUID: string, content: string) => { try { const response = await request.post(`/uuid/chat_sessions/${sessionUUID}/chat_messages/${messageUUID}/comments`, { content }) return response.data } catch (error) { console.error(error) throw error } } // return list of comments // comment (sessionUUID: string, messageUUID: string, content: string, createdAt: string) export const getConversationComments = async (sessionUUID: string) => { try { const response = await request.get(`/uuid/chat_sessions/${sessionUUID}/comments`) return response.data } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/api/content.ts ================================================ import { deleteChatMessage, updateChatMessage } from './chat_message' import { deleteChatPrompt, updateChatPrompt } from './chat_prompt' export const deleteChatData = async (chat: Chat.Message) => { if (chat?.isPrompt) await deleteChatPrompt(chat.uuid) else await deleteChatMessage(chat.uuid) } export const updateChatData = async (chat: Chat.Message) => { if (chat?.isPrompt) await updateChatPrompt(chat) else await updateChatMessage(chat) } ================================================ FILE: web/src/api/export.ts ================================================ import { getChatMessagesBySessionUUID } from './chat_message' function format_chat_md(chat: Chat.Message): string { return `${chat.dateTime}:\n ${chat.text}` } export const fetchMarkdown = async (uuid: string) => { try { const chatData = await getChatMessagesBySessionUUID(uuid) /* uuid: string, dateTime: string text: string inversion?: boolean error?: boolean loading?: boolean isPrompt?: boolean */ const markdown = chatData.map((chat: Chat.Message) => { if (chat.isPrompt) return `**system** ${format_chat_md(chat)}}` else if (chat.inversion) return `**user** ${format_chat_md(chat)}` else return `**assistant** ${format_chat_md(chat)}` }).join('\n\n----\n\n') return markdown } catch (error) { console.error(error) throw error } } export const fetchConversationSnapshot = async (uuid: string): Promise => { try { const chatData = await getChatMessagesBySessionUUID(uuid) /* uuid: string, dateTime: string text: string inversion?: boolean error?: boolean loading?: boolean isPrompt?: boolean */ return chatData } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/api/index.ts ================================================ export * from './admin' export * from './chat_user_model_privilege' export * from './chat_message' export * from './chat_model' export * from './chat_session' export * from './chat_workspace' export * from './chat_snapshot' export * from './chat_active_user_session' export * from './chat_instructions' export * from './content' export * from './export' export * from './user' ================================================ FILE: web/src/api/token.ts ================================================ import request from '@/utils/request/axios' export async function fetchAPIToken() { try { const response = await request.get('/token_10years') return response.data } catch (error) { throw error } } ================================================ FILE: web/src/api/use_chat_session.ts ================================================ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' import { createChatSession, deleteChatSession, getChatSessionsByUser, renameChatSession, updateChatSession } from './chat_session' // Get QueryClient from the context const queryClient = useQueryClient() // queryClient.invalidateQueries({ queryKey: ['sessions'] }) // query a session, when session updated, it will be invalidated const sessionListQuery = useQuery({ queryKey: ['sessions'], queryFn: getChatSessionsByUser, }) const createChatSessionQuery = useMutation({ mutationFn: (variables: { uuid: string, name: string, model?: string }) => createChatSession(variables.uuid, variables.name, variables.model), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sessions'] }) } }) const deleteChatSessionQuery = useMutation({ mutationFn: (uuid: string) => deleteChatSession(uuid), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sessions'] }) } }) const renameChatSessionQuery = useMutation({ mutationFn: (variables: { uuid: string, name: string }) => renameChatSession(variables.uuid, variables.name), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sessions'] }) } }) const updateChatSessionQuery = useMutation({ mutationFn: (variables: { sessionUuid: string, sessionData: Chat.Session }) => updateChatSession(variables.sessionUuid, variables.sessionData), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sessions'] }) } }) export { sessionListQuery, createChatSessionQuery, deleteChatSessionQuery, renameChatSessionQuery, updateChatSessionQuery } ================================================ FILE: web/src/api/user.ts ================================================ import request from '@/utils/request/axios' export async function fetchLogin(email: string, password: string) { try { const response = await request.post('/login', { email, password }) return response.data } catch (error) { console.error(error) throw error } } export async function fetchSignUp(email: string, password: string) { try { const response = await request.post('/signup', { email, password }) return response.data } catch (error) { console.error(error) throw error } } ================================================ FILE: web/src/assets/recommend.json ================================================ [ { "key": "awesome-chatgpt-prompts-zh", "desc": "ChatGPT 中文调教指南", "downloadUrl": "/static/awesome-chatgpt-prompts-zh.json", "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" }, { "key": "awesome-chatgpt-prompts-en", "desc": "ChatGPT English Prompts", "downloadUrl": "/static/awesome-chatgpt-prompts-en.json", "url": "https://github.com/f/awesome-chatgpt-prompts" } ] ================================================ FILE: web/src/components/admin/ModelCard.vue ================================================ ================================================ FILE: web/src/components/admin/SessionSnapshotModal.vue ================================================ ================================================ FILE: web/src/components/admin/UserAnalysisModal.vue ================================================ ================================================ FILE: web/src/components/common/EnhancedNotification.vue ================================================ ================================================ FILE: web/src/components/common/HoverButton/Button.vue ================================================ ================================================ FILE: web/src/components/common/HoverButton/index.vue ================================================ ================================================ FILE: web/src/components/common/NaiveProvider/index.vue ================================================ ================================================ FILE: web/src/components/common/NotificationDemo.vue ================================================ ================================================ FILE: web/src/components/common/PromptStore/index.vue ================================================ ================================================ FILE: web/src/components/common/Setting/Admin.vue ================================================ ================================================ FILE: web/src/components/common/Setting/General.vue ================================================ ================================================ FILE: web/src/components/common/Setting/index.vue ================================================ ================================================ FILE: web/src/components/common/SvgIcon/index.vue ================================================ ================================================ FILE: web/src/components/common/UserAvatar/index.vue ================================================ ================================================ FILE: web/src/components/common/index.ts ================================================ import HoverButton from './HoverButton/index.vue' import NaiveProvider from './NaiveProvider/index.vue' import SvgIcon from './SvgIcon/index.vue' import UserAvatar from './UserAvatar/index.vue' import Setting from './Setting/index.vue' import PromptStore from './PromptStore/index.vue' export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore } ================================================ FILE: web/src/components/custom/GithubSite.vue ================================================ ================================================ FILE: web/src/components/custom/index.ts ================================================ import GithubSite from './GithubSite.vue' export { GithubSite } ================================================ FILE: web/src/config/api.ts ================================================ interface ApiConfig { baseURL: string streamingURL: string } /** * Get API configuration based on environment */ export function getApiConfig(): ApiConfig { // Check for explicit configuration first const customBackendUrl = (import.meta as any).env?.VITE_BACKEND_URL const customStreamingUrl = (import.meta as any).env?.VITE_STREAMING_URL // If both are explicitly configured, use them if (customBackendUrl && customStreamingUrl) { return { baseURL: customBackendUrl, streamingURL: customStreamingUrl } } // Environment-based defaults const isDevelopment = process.env.NODE_ENV === 'development' if (isDevelopment) { // In development, use direct backend URL for streaming to bypass proxy buffering return { baseURL: '/api', // Use proxy for regular API calls streamingURL: customStreamingUrl || 'http://localhost:8080/api' // Direct connection for streaming } } // Production defaults return { baseURL: customBackendUrl || '/api', streamingURL: customStreamingUrl || '/api' } } /** * Get the appropriate URL for streaming endpoints */ export function getStreamingUrl(endpoint: string): string { const config = getApiConfig() return `${config.streamingURL}${endpoint}` } /** * Get the appropriate URL for regular API endpoints */ export function getApiUrl(endpoint: string): string { const config = getApiConfig() return `${config.baseURL}${endpoint}` } ================================================ FILE: web/src/constants/apiTypes.ts ================================================ export const API_TYPES = { OPENAI: 'openai', CLAUDE: 'claude', GEMINI: 'gemini', OLLAMA: 'ollama', CUSTOM: 'custom' } as const export type ApiType = typeof API_TYPES[keyof typeof API_TYPES] export const API_TYPE_OPTIONS = [ { label: 'OpenAI', value: API_TYPES.OPENAI }, { label: 'Claude', value: API_TYPES.CLAUDE }, { label: 'Gemini', value: API_TYPES.GEMINI }, { label: 'Ollama', value: API_TYPES.OLLAMA }, { label: 'Custom', value: API_TYPES.CUSTOM } ] export const API_TYPE_DISPLAY_NAMES = { [API_TYPES.OPENAI]: 'OpenAI', [API_TYPES.CLAUDE]: 'Claude', [API_TYPES.GEMINI]: 'Gemini', [API_TYPES.OLLAMA]: 'Ollama', [API_TYPES.CUSTOM]: 'Custom' } as const ================================================ FILE: web/src/constants/chat.ts ================================================ import { t } from '@/locales' export const getDefaultSystemPrompt = () => t('chat.defaultSystemPrompt') as string ================================================ FILE: web/src/hooks/useBasicLayout.ts ================================================ import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' export function useBasicLayout() { const breakpoints = useBreakpoints(breakpointsTailwind) const isMobile = breakpoints.smaller('sm') const isBigScreen = breakpoints.greater('2xl') return { isMobile, isBigScreen } } ================================================ FILE: web/src/hooks/useChatModels.ts ================================================ import { useQuery, useQueryClient, useMutation } from '@tanstack/vue-query' import { computed } from 'vue' import { useAuthStore } from '@/store' import { fetchChatModel, updateChatModel, deleteChatModel, createChatModel, fetchDefaultChatModel } from '@/api/chat_model' import type { ChatModel, CreateChatModelRequest, UpdateChatModelRequest } from '@/types/chat-models' export const useChatModels = () => { const authStore = useAuthStore() const queryClient = useQueryClient() const useChatModelsQuery = () => { return useQuery({ queryKey: ['chat_models'], queryFn: fetchChatModel, staleTime: 5 * 60 * 1000, // 5 minutes - reduced for better responsiveness enabled: computed(() => authStore.isInitialized && !authStore.isInitializing && authStore.isValid), }) } const useDefaultChatModelQuery = () => { return useQuery({ queryKey: ['chat_models', 'default'], queryFn: fetchDefaultChatModel, staleTime: 5 * 60 * 1000, // 5 minutes - reduced for better responsiveness enabled: computed(() => authStore.isInitialized && !authStore.isInitializing && authStore.isValid), }) } const useUpdateChatModelMutation = () => { return useMutation({ mutationFn: ({ id, data }) => updateChatModel(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['chat_models'] }) queryClient.invalidateQueries({ queryKey: ['chat_models', 'default'] }) }, }) } const useDeleteChatModelMutation = () => { return useMutation({ mutationFn: (id: number) => deleteChatModel(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['chat_models'] }) queryClient.invalidateQueries({ queryKey: ['chat_models', 'default'] }) }, }) } const useCreateChatModelMutation = () => { return useMutation({ mutationFn: (data: CreateChatModelRequest) => createChatModel(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['chat_models'] }) queryClient.invalidateQueries({ queryKey: ['chat_models', 'default'] }) }, }) } return { useChatModelsQuery, useDefaultChatModelQuery, useUpdateChatModelMutation, useDeleteChatModelMutation, useCreateChatModelMutation, } } ================================================ FILE: web/src/hooks/useCopyCode.ts ================================================ import { onMounted, onUpdated } from 'vue' import { copyText } from '@/utils/format' export function useCopyCode() { function copyCodeBlock() { const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper') codeBlockWrapper.forEach((wrapper) => { const copyBtn = wrapper.querySelector('.code-block-header__copy') const codeBlock = wrapper.querySelector('.code-block-body') if (copyBtn && codeBlock) { copyBtn.addEventListener('click', () => { if (navigator.clipboard?.writeText) navigator.clipboard.writeText(codeBlock.textContent ?? '') else copyText({ text: codeBlock.textContent ?? '', origin: true }) }) } }) } onMounted(() => copyCodeBlock()) onUpdated(() => copyCodeBlock()) } ================================================ FILE: web/src/hooks/useIconRender.ts ================================================ import { h } from 'vue' import { SvgIcon } from '@/components/common' export const useIconRender = () => { interface IconConfig { icon?: string color?: string fontSize?: number } interface IconStyle { color?: string fontSize?: string } const iconRender = (config: IconConfig) => { const { color, fontSize, icon } = config const style: IconStyle = {} if (color) style.color = color if (fontSize) style.fontSize = `${fontSize}px` if (!icon) window.console.warn('iconRender: icon is required') return () => h(SvgIcon, { icon, style }) } return { iconRender, } } ================================================ FILE: web/src/hooks/useLanguage.ts ================================================ import { computed } from 'vue' import { enUS, zhCN, zhTW } from 'naive-ui' import { useAppStore } from '@/store' import { setLocale } from '@/locales' export function useLanguage() { const appStore = useAppStore() const language = computed(() => { switch (appStore.language) { case 'en-US': setLocale('en-US') return enUS case 'zh-CN': setLocale('zh-CN') return zhCN case 'zh-TW': setLocale('zh-TW') return zhTW default: setLocale('zh-CN') return enUS } }) return { language } } ================================================ FILE: web/src/hooks/useOnlineStatus.ts ================================================ // src/composables/useOnlineStatus.js import { ref, onMounted, onUnmounted } from 'vue'; export function useOnlineStatus() { const isOnline = ref(navigator.onLine); const updateOnlineStatus = () => { isOnline.value = navigator.onLine; }; onMounted(() => { window.addEventListener('online', updateOnlineStatus); window.addEventListener('offline', updateOnlineStatus); }); onUnmounted(() => { window.removeEventListener('online', updateOnlineStatus); window.removeEventListener('offline', updateOnlineStatus); }); return { isOnline }; } ================================================ FILE: web/src/hooks/useTheme.ts ================================================ import type { GlobalThemeOverrides } from 'naive-ui' import { computed, watch } from 'vue' import { darkTheme, useOsTheme } from 'naive-ui' import { useAppStore } from '@/store' export function useTheme() { const appStore = useAppStore() const OsTheme = useOsTheme() const isDark = computed(() => { if (appStore.theme === 'auto') return OsTheme.value === 'dark' else return appStore.theme === 'dark' }) const theme = computed(() => { return isDark.value ? darkTheme : undefined }) const tooltipOverrides = { Tooltip: { color: '#4b9e5f', textColor: '#fff', }, } const themeOverrides = computed(() => { if (isDark.value) { return { common: {}, ...tooltipOverrides, } } return { ...tooltipOverrides } }) watch( () => isDark.value, (dark) => { if (dark) document.documentElement.classList.add('dark') else document.documentElement.classList.remove('dark') }, { immediate: true }, ) return { theme, themeOverrides } } ================================================ FILE: web/src/hooks/useWorkspaceRouting.ts ================================================ import { computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useSessionStore, useWorkspaceStore } from '@/store' export function useWorkspaceRouting() { const router = useRouter() const route = useRoute() const sessionStore = useSessionStore() const workspaceStore = useWorkspaceStore() // Get current workspace from URL const currentWorkspaceFromUrl = computed(() => { return route.params.workspaceUuid as string || null }) // Get current session from URL const currentSessionFromUrl = computed(() => { return route.params.uuid as string || null }) // Check if we're on a workspace-aware route const isWorkspaceRoute = computed(() => { return route.name === 'WorkspaceChat' }) // Generate workspace-aware URL for a session function getSessionUrl(sessionUuid: string, workspaceUuid?: string): string { const workspace = workspaceUuid || workspaceStore.activeWorkspace const session = sessionStore.getChatSessionByUuid(sessionUuid) // Use session's workspace if available, otherwise use provided or active workspace const targetWorkspace = session?.workspaceUuid || workspace || workspaceStore.getDefaultWorkspace?.uuid if (targetWorkspace) { return `/#/workspace/${targetWorkspace}/chat/${sessionUuid}` } // Fallback to default workspace if none found const defaultWorkspace = workspaceStore.getDefaultWorkspace return defaultWorkspace ? `/#/workspace/${defaultWorkspace.uuid}/chat/${sessionUuid}` : `/#/` } // Generate workspace URL (without session) function getWorkspaceUrl(workspaceUuid: string): string { return `/#/workspace/${workspaceUuid}/chat` } // Navigate to session with workspace context async function navigateToSession(sessionUuid: string, workspaceUuid?: string) { const workspace = workspaceUuid || workspaceStore.activeWorkspace const session = sessionStore.getChatSessionByUuid(sessionUuid) // Use session's workspace if available, otherwise use default workspace const targetWorkspace = session?.workspaceUuid || workspace || workspaceStore.getDefaultWorkspace?.uuid if (targetWorkspace) { await router.push({ name: 'WorkspaceChat', params: { workspaceUuid: targetWorkspace, uuid: sessionUuid } }) } else { // Fallback to default route if no workspace found await router.push({ name: 'DefaultWorkspace' }) } } // Navigate to workspace (without specific session) async function navigateToWorkspace(workspaceUuid: string) { await router.push({ name: 'WorkspaceChat', params: { workspaceUuid } }) } // Navigate to first session in workspace, or workspace itself if no sessions async function navigateToWorkspaceOrFirstSession(workspaceUuid: string) { const workspaceSessions = sessionStore.getSessionsByWorkspace(workspaceUuid) if (workspaceSessions.length > 0) { await navigateToSession(workspaceSessions[0].uuid, workspaceUuid) } else { await navigateToWorkspace(workspaceUuid) } } // Check if current route matches the expected workspace/session function isCurrentRoute(sessionUuid?: string, workspaceUuid?: string): boolean { const currentSession = currentSessionFromUrl.value const currentWorkspace = currentWorkspaceFromUrl.value if (sessionUuid && sessionUuid !== currentSession) { return false } if (workspaceUuid && workspaceUuid !== currentWorkspace) { return false } return true } // Sync URL with current state (useful for redirects after workspace changes) async function syncUrlWithState() { const activeSession = sessionStore.active const activeWorkspace = workspaceStore.activeWorkspace // If we have an active session and workspace, ensure URL is correct if (activeSession && activeWorkspace) { const session = sessionStore.getChatSessionByUuid(activeSession) if (session && session.workspaceUuid === activeWorkspace) { // Check if current URL doesn't match expected workspace-aware URL if (!isCurrentRoute(activeSession, activeWorkspace)) { await navigateToSession(activeSession, activeWorkspace) } } } } // Handle browser back/forward navigation function handleRouteChange() { const workspaceFromUrl = currentWorkspaceFromUrl.value const sessionFromUrl = currentSessionFromUrl.value // Update store state to match URL if (workspaceFromUrl && workspaceFromUrl !== workspaceStore.activeWorkspace) { workspaceStore.setActiveWorkspace(workspaceFromUrl) } if (sessionFromUrl && sessionFromUrl !== sessionStore.active) { const session = sessionStore.getChatSessionByUuid(sessionFromUrl) if (session) { sessionStore.setActiveSessionWithoutNavigation(session.workspaceUuid, sessionFromUrl) } } } return { // Computed currentWorkspaceFromUrl, currentSessionFromUrl, isWorkspaceRoute, // Methods getSessionUrl, getWorkspaceUrl, navigateToSession, navigateToWorkspace, navigateToWorkspaceOrFirstSession, isCurrentRoute, syncUrlWithState, handleRouteChange } } ================================================ FILE: web/src/icons/403.vue ================================================ ================================================ FILE: web/src/icons/500.vue ================================================ ================================================ FILE: web/src/locales/en-US-more.json ================================================ { "bot": { "noHistory": "No history yet", "runNumber": "Run {number}", "tabs": { "conversation": "Preset", "history": "Answer" } }, "error": { "MODEL_006": "Failed to get a response from the model" } } ================================================ FILE: web/src/locales/en-US.json ================================================ { "admin": { "activityHistory": "Activity History", "add_model": "Add Model", "add_user_model_rate_limit": "Add User Session Count", "chat_model": { "actions": "Actions", "apiAuthHeader": "Auth Header key", "apiAuthKey": "API KEY corresponding to the environment variable", "apiType": "API Type", "clear_form": "Clear Form", "copy": "Copy", "copy_success": "Copied successfully", "default": "Default", "defaultToken": "Default token number", "deleteModel": "Delete", "deleteModelConfirm": "Confirm deletion {name}?", "delete_failed": "Delete failed", "delete_success": "Delete success", "edit_model": "Edit Model", "enablePerModeRatelimit": "Enable Rate Limit Per Mode", "enablePerModelRateLimit": "Enable per-model rate limit", "isDefault": "Default?", "isEnable": "Is Enabled", "label": "Model name", "maxToken": "Maximum token number", "name": "Model ID", "orderNumber": "Order number", "paste_json": "Paste JSON Configuration", "paste_json_placeholder": "Paste your model configuration JSON here...", "populate_form": "Populate Form", "update_failed": "Update failed", "update_success": "Update success", "url": "Request full URL" }, "chat_model_name": "Model ID", "created": "Created", "date": "Date", "firstName": "First Name", "hideSubMenu": "Hide Submenu", "lastName": "Last Name", "lastUsed": "Last Used", "messages": "Messages", "messages3Days": "Messages (3 days)", "model": "Model", "modelUsage": "Model Usage", "modelUsageDistribution": "Model Usage Distribution", "model_one_default_only": "There can only be one default model, please set other models as non-default first.", "name": "Name", "openPanel": "Open Admin Panel", "overview": "Overview", "per_model_rate_limit": { "ChatModelName": "Model Name", "FullName": "Full Name", "RateLimit": "10min Access Count", "UserEmail": "User Email", "actions": "Actions" }, "per_model_rate_limit_title": "Model Throttling", "permission": "Permission", "rateLimit": "Rate Limit", "rateLimit10Min": "Message Limit (10 minutes)", "rate_limit": "Session Count (10min)", "recent3Days": "Recent 3 Days Activity", "refresh": "Refresh", "sessionHistory": "Session History", "sessionId": "Session ID", "sessionSnapshot": "Session Snapshot", "sessions": "Sessions", "showSubMenu": "Show Submenu", "system_model_tab_title": "Model Configuration", "title": "Admin", "tokens": "Tokens", "tokens3Days": "Tokens (3 days)", "totalChatMessages": "Total Chat Messages", "totalChatMessages3Days": "Total Chat Messages (3 days)", "totalChatMessages3DaysAvgTokenCount": "Average token count (3 days)", "totalChatMessages3DaysTokenCount": "Total token count (3 days)", "totalChatMessagesTokenCount": "Total token count", "totalMessages": "Total Messages", "totalSessions": "Total Sessions", "totalTokens": "Total Tokens", "updated": "Updated", "usage": "Usage", "userAnalysis": "User Analysis", "userEmail": "User Email", "userMessage": "User", "userStat": "User Statistics" }, "bot": { "all": { "title": "Bots" }, "list": "Bots", "noHistory": "No history yet", "runNumber": "Run {number}", "showCode": "Generate API call code", "tabs": { "conversation": "Preset", "history": "Answer" } }, "chat": { "N": "Number of results: {n}", "addComment": "Add Comment", "adjustParameters": "Adjust parameters", "advanced_settings": "Advanced Settings", "alreadyInNewChat": "alreay in new chat", "artifactMode": "Artifacts", "artifactModeDescription": "Enable artifact rendering for code, previews, and visualizations", "artifactInstructionTitle": "Artifact Instructions", "chatSettings": "Chat Settings", "chatSnapshot": "Generate the conversation", "clearChat": "Clear chat session", "clearChatConfirm": "Do you want to clear the chat session?", "clearHistoryConfirm": "Are you sure you want to clear the chat history?", "commentFailed": "Failed to add comment", "commentPlaceholder": "Enter your comment...", "commentSuccess": "Comment added successfully", "completionsCount": "Number of results: {contextCount}", "contextCount": "Context Length: {contextCount}", "contextLength": "Context Length, default 10 (2 at the beginning of the conversation + 8 most recent)", "copied": "Copied", "copy": "Copy", "copyCode": "Copy code", "createBot": "Create Bot", "debug": "Debug Mode", "debugDescription": "Enable debug mode for troubleshooting and diagnostics", "defaultSystemPrompt": "You are a helpful, concise assistant. Ask clarifying questions when needed. Provide accurate answers with short reasoning and actionable steps. If unsure, say so and suggest how to verify.", "deleteChatSessionsConfirm": "Are you sure you want to delete this record?", "deleteMessage": "Delete message", "deleteMessageConfirm": "Do you want to delete this message?", "disable_debug": "Disable", "disable_artifact": "Disable", "disable_explore": "Disable", "enable_debug": "Enable", "enable_artifact": "Enable", "enable_explore": "Enable", "exploreMode": "Explore Mode", "exploreModeDescription": "Get suggested questions based on conversation context", "loadingSession": "Loading session...", "loading_models": "Loading models...", "loading_instructions": "Loading instructions...", "modes": "Modes", "models": "models", "promptInstructions": "Prompt Instructions", "exportFailed": "Saving failed", "exportImage": "Export chat session to image", "exportImageConfirm": "Do you want to save the chat session as an image?", "exportMD": "Export chat session to markdown file", "exportMDConfirm": "Do you want to save the chat session as a markdown file?", "exportSuccess": "Saved successfully", "frequencyPenalty": "Frequency Penalty", "generateMoreSuggestions": "Generate more suggestions", "generating": "Generating...", "is_summarize_mode": "On", "maxTokens": "Max Total Tokens : {maxTokens}", "model": "Model", "new": "New Chat", "no_summarize_mode": "Off", "placeholder": "What would you like to say... (Shift + Enter = newline, '/' to trigger prompts)", "placeholderMobile": "What would you like to say...", "playAudio": "audio", "presencePenalty": "Presence Penalty", "sessionConfig": "Conversation Settings:", "snapshotSuccess": "Snapshot successful, please view in a new tab", "stopAnswer": "Stop Answering", "suggestedQuestions": "Suggested Questions", "summarize_mode": "Summary mode (supports longer context 20+)", "temperature": "Temperature : {temperature}", "topP": "Top P: {topP}", "turnOffContext": "In this mode, messages sent will not include previous chat logs.", "turnOnContext": "In this mode, messages sent will include previous chat logs.", "uploadFiles": "Upload Files", "uploader_button": "Upload", "uploader_close": "Close", "uploader_help_text": "Supported file types: text, image, audio, video", "uploader_title": "Upload File", "usingContext": "Context Mode" }, "chat_snapshot": { "createChat": "Create chat", "deleteFailed": "Unable to delete conversation record", "deletePost": "Are you sure you want to delete this conversation record?", "deletePostConfirm": "Delete conversation record", "deleteSuccess": "Conversation record deleted successfully", "exportImage": "Save conversation as image", "exportMarkdown": "Export to markdown file", "scrollTop": "Scroll to top", "showCode": "Generate API call code", "title": "Chat Records" }, "common": { "actions": "Actions", "ask_user_register": "Please register, only registered accounts can continue the conversation", "cancel": "Cancel", "clear": "Clear", "confirm": "Confirm", "copy": "Copy", "create": "Create", "delete": "Delete", "disabled": "Disabled", "edit": "Edit", "editUser": "Edit User", "email": "Email", "email_placeholder": "Please enter your email", "enabled": "Enabled", "export": "Export", "failed": "Operation failed", "fetchFailed": "Failed to fetch data", "help": "The first one is the theme(prompt), and the context includes 10 messages.", "import": "Import", "login": "Login", "loginSuccess": "Login Successful", "login_failed": "Login Failed", "logout": "Logout", "logout_failed": "Logout Failed", "logout_success": "Logout Successful", "no": "No", "noData": "No data available", "password_placeholder": "Please enter your password", "please_register": "Please Register First", "regenerate": "regenerate", "reset": "Reset", "save": "Save", "signup": "Sign up", "signup_failed": "Registration Failed", "signup_success": "Registration Successful", "submit": "Submit", "submitting": "Submitting...", "success": "Operation succeeded", "unauthorizedTips": "Please sign up or login", "update": "Update", "use": "Use", "verify": "Verify", "warning": "Warning", "wrong": "Something seems to have gone wrong. Please try again later.", "yes": "Yes" }, "error": { "INTN_004": "Failed to request the model, please try again later or contact the administrator", "MODEL_001": "the first message is a system message, please continue entering information to start the conversation", "MODEL_006": "Failed to get a response from the model", "NotAdmin": "Non-administrators are prohibited from accessing", "NotAuthorized": "Please log in first", "RESOURCE_EXHAUSTED": "Resource exhausted", "VALD_001": "Invalid Request", "VALD_004": "Invalid email or password", "connection": "Connection interrupted. Please check your connection and try again.", "fail_to_get_rate_limit": "Failed to retrieve the quota corresponding to the model, please contact the administrator to enable it", "forbidden": "Access denied. You don't have permission for this action.", "gpt-4_over_limit": "GPT4 message sending limit exceeded, please contact the administrator to enable or increase quota.", "invalidEmail": "Invalid email address", "invalidPassword": "Password is not valid, it should be length >=6 and include a number, a lowercase letter, an uppercase letter, and a special character", "invalidRepwd": "Duplicate passwords are inconsistent", "network": "Network connection error. Please check your internet connection.", "notFound": "The requested resource was not found.", "rateLimit": "The message sending limit has been reached, please contact the administrator.", "serverError": "Server error. Our team has been notified and is working on a fix.", "syncChatSession": "Sync Failed, Please Try Again Later", "timeout": "Request timed out. Please check your connection and try again.", "token_length_exceed_limit": "Total message length exceeds limit, please reduce context quantity, message length or start a new conversation.", "unauthorized": "Session expired. Please login again.", "unknown": "An unexpected error occurred. Please try again.", "validation": "Please check your input and try again." }, "prompt": { "add": "Add", "addFailed": "Add failed", "addSuccess": "Add successful", "clear": "Clear", "confirmClear": "Are you sure you want to clear?", "delete": "Delete", "deleteConfirm": "Are you sure you want to delete?", "deleteFailed": "Delete failed", "deleteSuccess": "Delete successful", "download": "Download", "downloadOnline": "Import from URL", "downloadOnlineWarning": "Warning: Please verify the source of the downloaded JSON file. Malicious JSON files could harm your computer!", "edit": "Edit", "editFailed": "Edit failed", "editSuccess": "Edit successful", "enterJsonUrl": "Please enter a valid JSON URL", "export": "Export", "import": "Import", "store": "Prompt" }, "setting": { "admin": "Admin", "api": "API", "apiToken": "API Token", "apiTokenCopied": "API Token copied", "apiTokenCopyFailed": "Failed to copy API Token", "avatarLink": "Avatar Link", "chatHistory": "Chat History", "config": "Configuration", "defaultDesc": "Signature", "defaultName": "You", "description": "Description", "general": "Overview", "language": "Language", "name": "Name", "resetUserInfo": "Reset User Info", "reverseProxy": "Reverse Proxy", "setting": "Settings", "snapshotLink": "Conversation history", "socks": "Socks", "switchLanguage": "English(切换语言) ", "theme": "Theme", "timeout": "Timeout" }, "workspace": { "active": "Active", "cannotDeleteDefault": "Cannot delete the default workspace", "color": "Color", "create": "Create Workspace", "createFirst": "Create your first workspace", "created": "Workspace created successfully", "default": "Default", "deleteConfirm": "Are you sure you want to delete this workspace?", "deleted": "Workspace deleted successfully", "description": "Description", "descriptionPlaceholder": "Optional description for this workspace", "dragToReorder": "Drag to reorder", "duplicate": "Duplicate", "edit": "Edit Workspace", "filteredResults": "of {total}", "icon": "Icon", "invalidColor": "Invalid color format. Please use a valid hex color.", "lastUpdated": "Updated", "loading": "Loading workspaces...", "manage": "Manage Workspaces", "name": "Name", "namePlaceholder": "Enter workspace name", "nameRequired": "Workspace name is required", "noWorkspaces": "No workspaces found", "reorderError": "Failed to reorder workspaces", "reorderMode": "Reorder Mode", "reorderSuccess": "Workspaces reordered successfully", "saveError": "Failed to save workspace", "searchPlaceholder": "Search workspaces...", "sessionCount": "{count} sessions", "setAsDefault": "Set as Default", "switchError": "Failed to switch workspace", "switchedTo": "Switched to {name}", "totalCount": "Total: {count} workspaces", "updated": "Workspace updated successfully" } } ================================================ FILE: web/src/locales/en.ts ================================================ ================================================ FILE: web/src/locales/index.ts ================================================ import type { App } from 'vue' import { createI18n } from 'vue-i18n' import enUS from './en-US.json' import zhCN from './zh-CN.json' import zhTW from './zh-TW.json' import type { Language } from '@/store/modules/app/helper' const i18n = createI18n({ locale: navigator.language.split('-')[0], fallbackLocale: 'en', allowComposition: true, messages: { 'en-US': enUS, 'zh-CN': zhCN, 'zh-TW': zhTW, }, }) export function t(key: string, values?: Record) { if (values) { return i18n.global.t(key, values) } else { return i18n.global.t(key) } } export function setLocale(locale: Language) { i18n.global.locale = locale } export function setupI18n(app: App) { app.use(i18n) } export default i18n ================================================ FILE: web/src/locales/zh-CN.json ================================================ { "common": { "ask_user_register": "请注册, 只有注册账号才能继续对话", "help": "第一条是主题(prompt, 角色定义), 上下文默认包括10条信息, 参数可以点击按钮进行调节, 请务必注意隐私, 不输入涉密, 敏感信息.", "copy": "复制", "edit": "编辑", "editUser": "编辑用户", "delete": "删除", "disabled": "禁用", "enabled": "启用", "actions": "操作", "warning": "警告", "save": "保存", "reset": "重置", "export": "导出", "import": "导入", "clear": "清空", "yes": "是", "no": "否", "use": "使用", "noData": "暂无数据", "wrong": "好像出错了,请稍后再试。", "success": "操作成功", "failed": "操作失败", "fetchFailed": "获取数据失败", "verify": "验证", "login": "登录", "logout": "登出", "regenerate": "重新生成", "signup": "注册", "email": "邮箱", "confirm": "确认", "cancel": "取消", "create": "创建", "update": "更新", "email_placeholder": "请输入邮箱", "password_placeholder": "请输入密码", "unauthorizedTips": "请注册或者登录", "loginSuccess": "登录成功", "logout_success": "登出成功", "signup_success": "注册成功", "signup_failed": "注册失败", "submit": "提交", "submitting": "提交中...", "login_failed": "登录失败", "logout_failed": "登出失败", "please_register": "请先注册" }, "prompt": { "store": "提示词", "add": "添加", "edit": "编辑", "delete": "删除", "deleteConfirm": "是否删除?", "deleteSuccess": "删除成功", "deleteFailed": "删除失败", "addSuccess": "添加成功", "addFailed": "添加失败", "editSuccess": "编辑成功", "editFailed": "编辑失败", "import": "导入", "export": "导出", "clear": "清空", "confirmClear": "是否清空?", "downloadOnline": "在线导入", "downloadOnlineWarning": "注意:请检查下载 JSON 文件来源,恶意的JSON文件可能会破坏您的计算机!", "download": "下载", "enterJsonUrl": "请输入正确的JSON地址" }, "bot": { "list": "机器人", "noHistory": "还没有记录", "runNumber": "Run {number}", "all": { "title": "机器人" }, "tabs": { "conversation": "预设", "history": "回答" }, "showCode": "生成API调用代码" }, "chat": { "alreadyInNewChat": "已经在新对话中", "advanced_settings": "高级设置", "artifactModeDescription": "启用代码、预览和可视化的 Artifact 渲染", "debugDescription": "启用调试模式用于故障排除和诊断", "defaultSystemPrompt": "你是一个有帮助且简明的助手。需要时先提出澄清问题。给出准确答案,并提供简短理由和可执行步骤。不确定时要说明,并建议如何验证。", "exploreModeDescription": "基于对话上下文获取建议问题", "loading_models": "正在加载模型...", "modes": "模式", "models": "个模型", "new": "新对话", "summarize_mode": "总结模式(可以支持更长的上下文20+)", "is_summarize_mode": "开启", "no_summarize_mode": "关闭", "placeholder": "来说点什么吧...(Shift + Enter = 换行, '/' 触发提示词)", "placeholderMobile": "来说点什么...", "copy": "复制", "copied": "复制成功", "copyCode": "复制代码", "clearChat": "清空会话", "clearChatConfirm": "是否清空会话?", "exportImage": "保存会话到图片", "chatSnapshot": "生成会话记录", "chatSettings": "对话设置", "uploadFiles": "上传文件", "createBot": "创建机器人", "snapshotSuccess": "快照成功, 请在新标签页中查看, 链接任何人都可以打开, 谨慎分享", "adjustParameters": "调整参数", "exportImageConfirm": "是否将会话保存为图片?", "exportMD": "保存会话到Markdown", "exportMDConfirm": "是否将会话保存为Markdown?", "exportSuccess": "保存成功", "exportFailed": "保存失败", "usingContext": "上下文模式", "turnOnContext": "当前模式下, 发送消息会携带之前的聊天记录", "turnOffContext": "当前模式下, 发送消息不会携带之前的聊天记录", "deleteMessage": "删除消息", "deleteMessageConfirm": "是否删除此消息?", "deleteChatSessionsConfirm": "确定删除此记录?", "clearHistoryConfirm": "确定清空聊天记录?", "contextLength": "上下文数量, 默认10 (会话开始的2条 + 最近的8条)", "stopAnswer": "停止回答", "suggestedQuestions": "深入提问", "contextCount": "上下文数量: {contextCount}", "temperature": "温度: {temperature}, 越高回答越发散, 越低回答越精确", "model": "模型", "maxTokens": "最大输出token数量: {maxTokens}", "topP": "Top P: {topP}, 常用词占比, 一般与温度参数只调节一个即可", "N": "结果数量: {n}", "frequencyPenalty": "频率惩罚", "presencePenalty": "存在惩罚", "debug": "调试模式", "artifactMode": "Artifacts", "sessionConfig": "会话设置", "enable_debug": "启用", "enable_artifact": "启用", "disable_debug": "关闭", "disable_artifact": "关闭", "exploreMode": "探索模式", "enable_explore": "启用", "disable_explore": "关闭", "promptInstructions": "提示词说明", "artifactInstructionTitle": "Artifact 说明", "loading_instructions": "正在加载说明...", "loadingSession": "正在加载会话...", "completionsCount": "结果数量: {contextCount}", "generateMoreSuggestions": "生成更多建议", "generating": "生成中...", "playAudio": "语音", "uploader_title": "上传文件", "uploader_button": "上传", "uploader_close": "关闭", "uploader_help_text": "支持上传文件类型: text, image, audio, video", "addComment": "添加评论", "commentPlaceholder": "请输入评论...", "commentSuccess": "评论添加成功", "commentFailed": "评论添加失败" }, "chat_snapshot": { "title": "会话集", "deletePost": "您确定要删除此会话记录吗?", "deletePostConfirm": "删除会话记录", "deleteSuccess": "会话记录删除成功", "deleteFailed": "无法删除会话记录", "exportImage": "保存会话到图片", "showCode": "生成API调用代码", "exportMarkdown": "导出到markdown文件", "createChat": "创建会话", "scrollTop": "回到顶部" }, "admin": { "openPanel": "在新标签页中打开", "title": "管理", "refresh": "刷新", "userEmail": "用户邮箱", "totalChatMessages": "消息总数", "totalChatMessages3Days": "消息总数(3天)", "totalChatMessagesTokenCount": "总token数量", "totalChatMessages3DaysTokenCount": "总token数量(3天)", "totalChatMessages3DaysAvgTokenCount": "平均token数量(3天)", "firstName": "名字", "lastName": "姓氏", "hideSubMenu": "隐藏子菜单", "showSubMenu": "显示子菜单", "userStat": "用户统计", "permission": "权限", "rateLimit": "限流", "userMessage": "用户", "userAnalysis": "用户分析", "overview": "概览", "modelUsage": "模型使用情况", "activityHistory": "活动历史", "sessionHistory": "会话历史", "totalMessages": "总消息数", "totalTokens": "总令牌数", "totalSessions": "总会话数", "recent3Days": "最近3天活动", "messages3Days": "消息数(3天)", "tokens3Days": "令牌数(3天)", "modelUsageDistribution": "模型使用分布", "messages": "消息", "tokens": "令牌", "usage": "使用率", "lastUsed": "最后使用", "date": "日期", "sessions": "会话", "sessionId": "会话ID", "created": "创建时间", "updated": "更新时间", "sessionSnapshot": "会话快照", "model": "模型", "system_model_tab_title": "模型配置", "per_model_rate_limit_title": "模型流控", "add_user_model_rate_limit": "添加用户会话数量", "add_model": "添加模型", "rate_limit": "会话数量(10min)", "chat_model_name": "模型ID", "chat_model": { "copy": "复制", "copy_success": "复制成功", "default": "默认", "deleteModel": "删除", "deleteModelConfirm": "确认删除 {name}?", "name": "模型ID", "label": "标签", "isDefault": "是否默认", "apiAuthHeader": "AuthHeader", "apiAuthKey": "API密钥环境变量", "apiType": "API类型", "actions": "操作", "url": "访问接口(完整URL路径)", "enablePerModeRatelimit": "是否单独流控", "isEnable": "是否启用", "orderNumber": "排序号", "maxToken": "最大输出token数量", "defaultToken": "默认输出token数量", "paste_json": "粘贴JSON配置", "paste_json_placeholder": "在此处粘贴您的模型配置JSON...", "populate_form": "填充表单", "clear_form": "清空表单" }, "per_model_rate_limit": { "FullName": "姓名", "UserEmail": "用户邮箱", "ChatModelName": "模型名称(ID)", "RateLimit": "10分钟访问次数", "actions": "操作" }, "model_one_default_only": "只能有一个默认模型, 请先设置其他模型为非默认", "rateLimit10Min": "消息数量上限(10分钟)", "name": "姓名" }, "setting": { "setting": "设置", "general": "总览", "admin": "管理", "config": "配置", "avatarLink": "头像链接", "name": "名称", "defaultName": "你", "defaultDesc": "签名", "snapshotLink": "会话集", "apiToken": "API Token", "description": "描述", "resetUserInfo": "重置用户信息", "chatHistory": "聊天记录", "theme": "主题", "language": "语言", "switchLanguage": "切换语言(English)", "api": "API", "reverseProxy": "反向代理", "timeout": "超时", "socks": "Socks", "apiTokenCopied": "API Token 已复制", "apiTokenCopyFailed": "无法复制 API Token" }, "error": { "MODEL_001": "第一条是系统消息已经是收到, 请继续输入信息开始会话", "MODEL_006": "无法从模型获取回复", "RESOURCE_EXHAUSTED": "资源耗尽", "VALD_001": "无效请求", "VALD_004": "无效的电子邮件或密码", "INTN_004": "请求模型失败, 请稍后再试, 或联系管理员", "rateLimit": "发送消息数量已经达到上限, 请联系管理员", "fail_to_get_rate_limit": "无法查询到模型对应的配额, 请联系管理员开通", "gpt-4_over_limit": "gpt4 发送消息达到上限, 请联系管理员开通,或增加额度", "token_length_exceed_limit": "总消息长度数量超过上限, 请减少上下文数量、减少消息长度或者开启新的会话", "invalidEmail": "邮箱格式不正确", "invalidPassword": "密码格式不正确, 长度至少为6位, 一个大写字母, 一个小写字母, 一个数字, 一个特殊字符", "invalidRepwd": "重复密码不一致", "syncChatSession": "同步失败, 请稍后再试", "NotAuthorized": "请先登录", "NotAdmin": "非管理员禁止访问" }, "workspace": { "create": "创建工作区", "edit": "编辑工作区", "manage": "管理工作区", "loading": "加载工作区中...", "name": "名称", "namePlaceholder": "输入工作区名称", "nameRequired": "工作区名称为必填项", "description": "描述", "descriptionPlaceholder": "为此工作区添加可选描述", "icon": "图标", "color": "颜色", "saveError": "保存工作区失败", "deleteConfirm": "确定要删除此工作区吗?", "cannotDeleteDefault": "无法删除默认工作区", "switchedTo": "已切换到 {name}", "created": "工作区创建成功", "updated": "工作区更新成功", "deleted": "工作区删除成功", "searchPlaceholder": "搜索工作区...", "totalCount": "共 {count} 个工作区", "filteredResults": "/ {total}", "noWorkspaces": "未找到工作区", "createFirst": "创建您的第一个工作区", "duplicate": "复制", "setAsDefault": "设为默认", "active": "当前", "default": "默认", "sessionCount": "{count} 个会话", "lastUpdated": "更新于", "switchError": "切换工作区失败", "reorderMode": "排序模式", "dragToReorder": "拖拽排序", "reorderSuccess": "工作区排序成功", "reorderError": "工作区排序失败", "invalidColor": "颜色格式无效。请使用有效的十六进制颜色。" } } ================================================ FILE: web/src/locales/zh-TW-more.json ================================================ { "common": { "disabled": "停用", "enabled": "啟用", "create": "建立", "update": "更新" }, "chat": { "alreadyInNewChat": "已在新的對話中", "generateMoreSuggestions": "產生更多建議", "generating": "生成中..." }, "admin": { "chat_model": { "default": "預設", "apiType": "API 類型" } }, "workspace": { "create": "建立工作區", "edit": "編輯工作區", "manage": "管理工作區", "loading": "載入工作區中...", "name": "名稱", "namePlaceholder": "輸入工作區名稱", "nameRequired": "工作區名稱為必填項", "description": "描述", "descriptionPlaceholder": "為此工作區添加可選描述", "icon": "圖示", "color": "顏色", "saveError": "儲存工作區失敗", "deleteConfirm": "確定要刪除此工作區嗎?", "cannotDeleteDefault": "無法刪除預設工作區", "switchedTo": "已切換至 {name}", "created": "工作區建立成功", "updated": "工作區更新成功", "deleted": "工作區刪除成功", "searchPlaceholder": "搜尋工作區...", "totalCount": "共 {count} 個工作區", "filteredResults": "/ {total}", "noWorkspaces": "未找到工作區", "createFirst": "建立您的第一個工作區", "duplicate": "複製", "setAsDefault": "設為預設", "active": "目前", "default": "預設", "sessionCount": "{count} 個會話", "lastUpdated": "更新於", "switchError": "切換工作區失敗", "reorderMode": "排序模式", "dragToReorder": "拖曳排序", "reorderSuccess": "工作區排序成功", "reorderError": "工作區排序失敗", "invalidColor": "顏色格式無效。請使用有效的十六進制顏色。" } } ================================================ FILE: web/src/locales/zh-TW.json ================================================ { "admin": { "activityHistory": "活動歷史", "add_model": "添加模型", "add_user_model_rate_limit": "添加用戶會話數量", "chat_model": { "actions": "操作", "apiAuthHeader": "Auth Header 鍵", "apiAuthKey": "API KEY對應的環境變量", "apiType": "API 類型", "clear_form": "清空表單", "copy": "複製", "copy_success": "複製成功", "default": "預設", "defaultToken": "默認token數量", "deleteModel": "刪除", "deleteModelConfirm": "確認刪除 {name}?", "enablePerModeRatelimit": "是否單獨流控", "enablePerModelRateLimit": "是否單獨流控", "isDefault": "默認?", "isEnable": "是否啟用", "label": "模型名稱(ID)", "maxToken": "最大token數量", "name": "身分識別號", "orderNumber": "次序", "paste_json": "貼上 JSON 配置", "paste_json_placeholder": "在此處貼上您的模型配置 JSON...", "populate_form": "填充表單", "url": "完整的URL請求" }, "chat_model_name": "模型ID", "created": "建立時間", "date": "日期", "firstName": "名字", "hideSubMenu": "隱藏子選單", "lastName": "姓氏", "lastUsed": "最後使用", "messages": "訊息", "messages3Days": "訊息數(3天)", "model": "模型", "modelUsage": "模型使用情況", "modelUsageDistribution": "模型使用分佈", "model_one_default_only": "只能有一個默認模型,請先將其他模型設置為非默認", "name": "姓名", "openPanel": "打開管理面板", "overview": "概覽", "per_model_rate_limit": { "ChatModelName": "模型名稱", "FullName": "姓名", "RateLimit": "10分鐘訪問次數", "UserEmail": "用戶郵箱", "actions": "操作" }, "per_model_rate_limit_title": "模型流控", "permission": "權限", "rateLimit": "限流", "rateLimit10Min": "訊息數量上限(10分鐘)", "rate_limit": "會話數量(10min)", "recent3Days": "最近3天活動", "refresh": "重新整理", "sessionHistory": "會話歷史", "sessionId": "會話ID", "sessionSnapshot": "會話快照", "sessions": "會話", "showSubMenu": "顯示子選單", "system_model_tab_title": "模型配置", "title": "管理", "tokens": "令牌", "tokens3Days": "令牌數(3天)", "totalChatMessages": "訊息總數", "totalChatMessages3Days": "訊息總數(3天)", "totalChatMessages3DaysAvgTokenCount": "平均token數量(3天)", "totalChatMessages3DaysTokenCount": "總token數量(3天)", "totalChatMessagesTokenCount": "總token數量", "totalMessages": "總訊息數", "totalSessions": "總會話數", "totalTokens": "總令牌數", "updated": "更新時間", "usage": "使用率", "userAnalysis": "用戶分析", "userEmail": "用戶電子郵件", "userMessage": "用戶", "userStat": "用戶統計" }, "bot": { "all": { "title": "機器人" }, "list": "機器人", "noHistory": "還沒有紀錄", "runNumber": "執行 {number}", "showCode": "產生 API 呼叫程式碼", "tabs": { "conversation": "預設", "history": "紀錄" } }, "chat": { "N": "結果數量: {n}", "addComment": "新增評論", "adjustParameters": "調整參數", "advanced_settings": "高級設置", "alreadyInNewChat": "已在新的對話中", "artifactModeDescription": "啟用代碼、預覽和可視化的 Artifact 渲染", "chatSettings": "對話設定", "debugDescription": "啟用調試模式用於故障排除和診斷", "defaultSystemPrompt": "你是一個有幫助且簡明的助手。需要時先提出澄清問題。給出準確答案,並提供簡短理由和可執行步驟。不確定時要說明,並建議如何驗證。", "exploreModeDescription": "基於對話上下文獲取建議問題", "loading_models": "正在載入模型...", "modes": "模式", "models": "個模型", "chatSnapshot": "生成會話記錄", "clearChat": "清空對話", "clearChatConfirm": "是否清空對話?", "clearHistoryConfirm": "確定清空聊天記錄?", "commentFailed": "評論新增失敗", "commentPlaceholder": "請輸入評論...", "commentSuccess": "評論新增成功", "completionsCount": "結果數量: {contextCount}", "contextCount": "上下文數量: {contextCount}", "contextLength": "上下文數量,預設10(會話開始的2條 + 最近的8條)", "copied": "複製成功", "copy": "複製", "copyCode": "複製代碼", "createBot": "建立機器人", "debug": "調試模式", "artifactMode": "Artifacts", "deleteChatSessionsConfirm": "確定刪除此記錄?", "deleteMessage": "刪除消息", "deleteMessageConfirm": "是否刪除此消息?", "disable_debug": "關閉", "disable_artifact": "關閉", "disable_explore": "關閉", "enable_debug": "開啟", "enable_artifact": "開啟", "enable_explore": "開啟", "exploreMode": "探索模式", "promptInstructions": "提示詞說明", "artifactInstructionTitle": "Artifact 說明", "loading_instructions": "正在載入說明...", "loadingSession": "正在載入會話...", "exportFailed": "導出失敗", "exportImage": "導出對話到圖片", "exportImageConfirm": "是否將對話導出為圖片?", "exportMD": "導出對話到Markdown", "exportMDConfirm": "是否將對話導出為Markdown?", "exportSuccess": "導出成功", "frequencyPenalty": "頻率懲罰", "generateMoreSuggestions": "產生更多建議", "generating": "生成中...", "is_summarize_mode": "開啟", "maxTokens": "最大問答總token數量: {maxTokens}", "model": "模型", "new": "新建聊天", "no_summarize_mode": "關閉", "placeholder": "說些什麼...(Shift + Enter = 換行, '/' 触发提示词)", "placeholderMobile": "說些什麼...", "playAudio": "語音", "presencePenalty": "存在懲罰", "sessionConfig": "會話配置", "snapshotSuccess": "快照成功,請在新標籤頁中查看", "stopAnswer": "停止回答", "suggestedQuestions": "建議問題", "summarize_mode": "總結模式(可以支持更長的上下文20+)", "temperature": "溫度: {temperature}", "topP": "Top P: {topP}", "turnOffContext": "在此模式下,發送的消息將不包括以前的聊天記錄。", "turnOnContext": "在此模式下,發送的消息將包括以前的聊天記錄。", "uploadFiles": "上傳檔案", "uploader_button": "上傳", "uploader_close": "關閉", "uploader_help_text": "支援上傳檔案類型: text、image、audio、video", "uploader_title": "上傳檔案", "usingContext": "上下文模式" }, "chat_snapshot": { "createChat": "創建會話", "deleteFailed": "无法删除会话记录", "deletePost": "您确定要删除此会话记录吗?", "deletePostConfirm": "删除会话记录", "deleteSuccess": "会话记录删除成功", "exportImage": "保存對話到圖片", "exportMarkdown": "匯出到Markdown檔", "scrollTop": "回到頂部", "showCode": "產生 API 呼叫程式碼", "title": "會話集" }, "common": { "actions": "操作", "ask_user_register": "請註冊, 只有註冊帳號才能繼續對話", "cancel": "取消", "clear": "清空", "confirm": "確認", "copy": "複製", "create": "建立", "delete": "刪除", "disabled": "停用", "edit": "編輯", "editUser": "編輯用戶", "email": "郵箱", "email_placeholder": "請輸入電子郵件", "enabled": "啟用", "export": "匯出", "failed": "操作失敗", "fetchFailed": "獲取數據失敗", "help": "第一個是主題(prompt),上下文包括10條信息", "import": "匯入", "login": "登錄", "loginSuccess": "登入成功", "login_failed": "登入失敗", "logout": "登出", "logout_failed": "登出失敗", "logout_success": "登出成功", "no": "否", "noData": "暫無數據", "password_placeholder": "請輸入密碼", "please_register": "請先註冊", "reset": "重置", "regenerate": "重新生成", "save": "保存", "signup": "註冊", "signup_failed": "註冊失敗", "signup_success": "註冊成功", "submit": "提交", "submitting": "提交中...", "success": "操作成功", "unauthorizedTips": "請註冊或登錄", "update": "更新", "use": "使用", "verify": "註冊", "warning": "警告", "wrong": "好像出錯了,請稍後再試。", "yes": "是" }, "error": { "INTN_004": "請求模型失敗, 請稍後再試, 或聯繫管理員", "MODEL_001": "第一條是系統消息,請繼續輸入信息開始會話", "MODEL_006": "無法從模型取得回覆", "NotAdmin": "非管理員禁止訪問", "NotAuthorized": "請先登入", "RESOURCE_EXHAUSTED": "資源耗盡", "VALD_001": "無效請求", "VALD_004": "無效的電子郵件或密碼", "fail_to_get_rate_limit": "無法查詢模型對應的配額,請聯繫管理員開通", "gpt-4_over_limit": "gpt4 發送消息達到上限,請聯繫管理員開通,或增加額度", "invalidEmail": "電子郵件格式不正確", "invalidPassword": "密碼格式不正確,長度至少需為6位,應包含1個大寫字母、1個小寫字母、1個數字、1個特殊字符", "invalidRepwd": "重復密碼不一致", "rateLimit": "發送消息數量已經達到上限,請聯繫管理員", "syncChatSession": "同步失敗,請稍後再試", "token_length_exceed_limit": "總消息長度數量超過上限,請減少上下文數量、減少消息長度或者開啟新的會話" }, "prompt": { "add": "新增", "addFailed": "新增失敗", "addSuccess": "新增成功", "clear": "清空", "confirmClear": "是否清空?", "delete": "刪除", "deleteConfirm": "是否刪除?", "deleteFailed": "刪除失敗", "deleteSuccess": "刪除成功", "download": "下載", "downloadOnline": "線上匯入", "downloadOnlineWarning": "注意:請檢查下載 JSON 檔案來源,惡意的 JSON 檔案可能會破壞您的電腦!", "edit": "編輯", "editFailed": "編輯失敗", "editSuccess": "編輯成功", "enterJsonUrl": "請輸入正確的 JSON 位址", "export": "匯出", "import": "匯入", "store": "提示词" }, "setting": { "admin": "管理", "api": "API", "apiToken": "API Token", "apiTokenCopied": "API Token 已複製", "apiTokenCopyFailed": "無法複製 API Token", "avatarLink": "頭像鏈接", "chatHistory": "聊天記錄", "config": "配置", "defaultDesc": "签名", "defaultName": "你", "description": "描述", "general": "總覽", "language": "語言", "name": "名稱", "resetUserInfo": "重置用戶信息", "reverseProxy": "反向代理", "setting": "設置", "snapshotLink": "會话集", "socks": "Socks", "switchLanguage": "English(切换语言)", "theme": "主題", "timeout": "超時" }, "workspace": { "active": "目前", "cannotDeleteDefault": "無法刪除預設工作區", "color": "顏色", "create": "建立工作區", "createFirst": "建立您的第一個工作區", "created": "工作區建立成功", "default": "預設", "deleteConfirm": "確定要刪除此工作區嗎?", "deleted": "工作區刪除成功", "description": "描述", "descriptionPlaceholder": "為此工作區添加可選描述", "dragToReorder": "拖曳排序", "duplicate": "複製", "edit": "編輯工作區", "filteredResults": "/ {total}", "icon": "圖示", "invalidColor": "顏色格式無效。請使用有效的十六進制顏色。", "lastUpdated": "更新於", "loading": "載入工作區中...", "manage": "管理工作區", "name": "名稱", "namePlaceholder": "輸入工作區名稱", "nameRequired": "工作區名稱為必填項", "noWorkspaces": "未找到工作區", "reorderError": "工作區排序失敗", "reorderMode": "排序模式", "reorderSuccess": "工作區排序成功", "saveError": "儲存工作區失敗", "searchPlaceholder": "搜尋工作區...", "sessionCount": "{count} 個會話", "setAsDefault": "設為預設", "switchError": "切換工作區失敗", "switchedTo": "已切換至 {name}", "totalCount": "共 {count} 個工作區", "updated": "工作區更新成功" } } ================================================ FILE: web/src/main.ts ================================================ import { createApp } from 'vue' import { VueQueryPlugin } from '@tanstack/vue-query' import App from './App.vue' import { setupI18n } from './locales' import { setupAssets } from './plugins' import { setupStore } from './store' import { setupRouter } from './router' async function bootstrap() { const app = createApp(App) setupAssets() setupStore(app) setupI18n(app) await setupRouter(app) app.use(VueQueryPlugin) app.mount('#app') } bootstrap() ================================================ FILE: web/src/plugins/assets.ts ================================================ import 'katex/dist/katex.min.css' import '@/styles/lib/tailwind.css' import '@/styles/lib/highlight.less' import '@/styles/lib/github-markdown.less' import '@/styles/global.less' /** Tailwind's Preflight Style Override */ function naiveStyleOverride() { const meta = document.createElement('meta') meta.name = 'naive-ui-style' document.head.appendChild(meta) } function setupAssets() { naiveStyleOverride() } export default setupAssets ================================================ FILE: web/src/plugins/index.ts ================================================ import setupAssets from './assets' export { setupAssets } ================================================ FILE: web/src/router/index.ts ================================================ import type { App } from 'vue' import type { RouteRecordRaw } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router' import { setupPageGuard } from './permission' import { ChatLayout } from '@/views/chat/layout' const routes: RouteRecordRaw[] = [ { path: '/snapshot', name: 'Snapshot', component: () => import('@/views/snapshot/page.vue'), children: [ { path: ':uuid?', name: 'Snapshot', component: () => import('@/views/snapshot/page.vue'), }, ], }, { path: '/prompt/new', name: 'Prompt', component: () => import('@/views/prompt/creator.vue') }, { path: '/bot', name: 'Bot', component: () => import('@/views/bot/page.vue'), children: [ { path: ':uuid?', name: 'Bot', component: () => import('@/views/bot/page.vue'), }, ], }, { path: '/snapshot_all', name: 'SnapshotAll', component: () => import('@/views/snapshot/all.vue'), }, { path: '/bot_all', name: 'BotAll', component: () => import('@/views/bot/all.vue'), }, { path: '/admin', name: 'Admin', component: () => import('@/views/admin/index.vue'), children: [ { path: 'user', name: 'AdminUser', component: () => import('@/views/admin/user/index.vue'), }, { path: 'model', name: 'AdminModel', component: () => import('@/views/admin/model/index.vue'), }, { path: 'model_rate_limit', name: 'ModelRateLimit', component: () => import('@/views/admin/modelRateLimit/index.vue'), } ], }, { path: '/', name: 'Root', component: ChatLayout, children: [ { path: '/workspace/:workspaceUuid/chat/:uuid?', name: 'WorkspaceChat', component: () => import('@/views/chat/index.vue'), props: true, }, { path: '/workspace/:workspaceUuid', redirect: to => { // Redirect workspace-only URLs to include /chat return `/workspace/${to.params.workspaceUuid}/chat` } }, { path: '/', name: 'DefaultWorkspace', component: () => import('@/views/chat/index.vue'), props: true, }, ], }, { path: '/404', name: '404', component: () => import('@/views/exception/404/index.vue'), }, { path: '/500', name: '500', component: () => import('@/views/exception/500/index.vue'), }, { path: '/:pathMatch(.*)*', name: 'notFound', redirect: '/404', }, ] // !!! // https://router.vuejs.org/guide/essentials/history-mode.html // createWebHashHistory // It uses a hash character (#) before the actual URL that is internally passed. // Because this section of the URL is never sent to the server, // it doesn't require any special treatment on the server level. // It does however have a bad impact in SEO. If that's a concern for you, use the HTML5 history mode. // this is crazy, router in frontend is a nightmare export const router = createRouter({ history: createWebHashHistory(), routes, scrollBehavior: () => ({ left: 0, top: 0 }), }) setupPageGuard(router) export async function setupRouter(app: App) { app.use(router) await router.isReady() } ================================================ FILE: web/src/router/permission.ts ================================================ import type { Router } from 'vue-router' import { useAuthStore } from '@/store/modules/auth' import { useWorkspaceStore } from '@/store/modules/workspace' import { useSessionStore } from '@/store/modules/session' import { store } from '@/store' const FIVE_MINUTES_IN_SECONDS = 5 * 60 // Attempt to ensure we have a valid access token before continuing navigation async function ensureFreshToken(authStore: any) { const currentTs = Math.floor(Date.now() / 1000) const expiresIn = authStore.getExpiresIn const token = authStore.getToken // the user hasn’t logged in if (!token && !expiresIn) return // If we already have a token that is valid for some time, nothing to do if (token && expiresIn && expiresIn > currentTs + FIVE_MINUTES_IN_SECONDS) return try { await authStore.refreshToken() } catch (error) { // If refresh fails, make sure state is cleared so UI can prompt user authStore.removeToken() authStore.removeExpiresIn() } } export function setupPageGuard(router: Router) { router.beforeEach(async (to, from, next) => { const auth_store = useAuthStore(store) await ensureFreshToken(auth_store) // Handle workspace context from URL if (to.name === 'WorkspaceChat' && to.params.workspaceUuid) { const workspaceStore = useWorkspaceStore(store) const sessionStore = useSessionStore(store) const workspaceUuid = to.params.workspaceUuid as string // Only set active workspace if it's different and not already loading if (workspaceUuid !== workspaceStore.activeWorkspaceUuid && !workspaceStore.isLoading) { console.log('Setting workspace from URL:', workspaceUuid) await workspaceStore.setActiveWorkspace(workspaceUuid) } // Set active session if provided in URL if (to.params.uuid) { const sessionUuid = to.params.uuid as string if (sessionUuid !== sessionStore.activeSessionUuid) { await sessionStore.setActiveSession(workspaceUuid, sessionUuid) } } } // Handle default route - let store sync handle navigation to default workspace if (to.name === 'DefaultWorkspace') { console.log('On default route, letting store handle workspace navigation') } next() }) } ================================================ FILE: web/src/service/snapshot.ts ================================================ import { displayLocaleDate, formatYearMonth } from '@/utils/date' export function generateAPIHelper(uuid: string, apiToken: string, origin: string) { const data = { "message": "Your message here", "snapshot_uuid": uuid, "stream": false, } return `curl -X POST ${origin}/api/chatbot -H "Content-Type: application/json" -H "Authorization: Bearer ${apiToken}" -d '${JSON.stringify(data)}'` } export function getChatbotPosts(posts: Snapshot.Snapshot[]) { return posts .filter((post: Snapshot.Snapshot) => post.typ === 'chatbot') .map((post: Snapshot.Snapshot): Snapshot.PostLink => ({ uuid: post.uuid, date: displayLocaleDate(post.createdAt), title: post.title, })) } export function getSnapshotPosts(posts: Snapshot.Snapshot[]) { return posts .filter((post: Snapshot.Snapshot) => post.typ === 'snapshot') .map((post: Snapshot.Snapshot): Snapshot.PostLink => ({ uuid: post.uuid, date: displayLocaleDate(post.createdAt), title: post.title, })) } export function postsByYearMonthTransform(posts: Snapshot.PostLink[]) { const init: Record = {} return posts.reduce((acc, post) => { const yearMonth = formatYearMonth(new Date(post.date)) if (!acc[yearMonth]) acc[yearMonth] = [] acc[yearMonth].push(post) return acc }, init) } export function getSnapshotPostLinks(snapshots: Snapshot.Snapshot[]): Record { const snapshotPosts = getSnapshotPosts(snapshots) return postsByYearMonthTransform(snapshotPosts) } export function getBotPostLinks(bots: Snapshot.Snapshot[]): Record { const chatbotPosts = getChatbotPosts(bots) return postsByYearMonthTransform(chatbotPosts) } ================================================ FILE: web/src/services/codeTemplates.ts ================================================ /** * Code Templates and Snippets Service * Manages pre-built code templates and user-defined snippets */ import { reactive, computed } from 'vue' export interface CodeTemplate { id: string name: string description: string language: string code: string category: string tags: string[] author?: string createdAt: string updatedAt: string difficulty: 'beginner' | 'intermediate' | 'advanced' estimatedRunTime?: number requirements?: string[] isBuiltIn: boolean usageCount: number rating?: number examples?: { input?: string output?: string description?: string }[] } export interface TemplateCategory { id: string name: string description: string icon: string color: string templates: CodeTemplate[] } class CodeTemplatesService { private static instance: CodeTemplatesService private templates: CodeTemplate[] = reactive([]) private categories: TemplateCategory[] = reactive([]) private storageKey = 'code-templates' private userTemplatesKey = 'user-code-templates' private constructor() { this.initializeBuiltInTemplates() this.loadUserTemplates() } static getInstance(): CodeTemplatesService { if (!CodeTemplatesService.instance) { CodeTemplatesService.instance = new CodeTemplatesService() } return CodeTemplatesService.instance } /** * Initialize built-in templates */ private initializeBuiltInTemplates(): void { const builtInTemplates: CodeTemplate[] = [ // JavaScript Templates { id: 'js-hello-world', name: 'Hello World', description: 'Basic Hello World example', language: 'javascript', code: `console.log('Hello, World!') console.log('Welcome to JavaScript!')`, category: 'basics', tags: ['beginner', 'console', 'output'], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), difficulty: 'beginner', estimatedRunTime: 50, isBuiltIn: true, usageCount: 0, rating: 5, examples: [ { output: 'Hello, World!\nWelcome to JavaScript!', description: 'Simple console output' } ] }, { id: 'js-async-fetch', name: 'Async Data Fetching', description: 'Demonstrate async/await with simulated API calls', language: 'javascript', code: `// Simulate an API call async function fetchData(url) { console.log(\`Fetching data from: \${url}\`) // Simulate network delay await new Promise(resolve => setTimeout(resolve, 1000)) // Simulate API response return { data: [1, 2, 3, 4, 5], timestamp: new Date().toISOString(), status: 'success' } } // Using async/await async function main() { try { console.log('Starting data fetch...') const result = await fetchData('https://api.example.com/data') console.log('Data received:', result) console.log('Processing complete!') } catch (error) { console.error('Error fetching data:', error) } } main()`, category: 'async', tags: ['async', 'await', 'promises', 'intermediate'], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), difficulty: 'intermediate', estimatedRunTime: 1200, isBuiltIn: true, usageCount: 0, rating: 4.5 }, { id: 'js-canvas-animation', name: 'Canvas Animation', description: 'Create animated graphics using Canvas API', language: 'javascript', code: `// Create a canvas for animation const canvas = createCanvas(400, 300) const ctx = canvas.getContext('2d') // Animation parameters let frame = 0 const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'] // Animation loop function animate() { // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height) // Draw animated circles for (let i = 0; i < 5; i++) { const x = 200 + Math.sin(frame * 0.02 + i) * 100 const y = 150 + Math.cos(frame * 0.03 + i) * 50 const radius = 20 + Math.sin(frame * 0.05 + i) * 10 ctx.beginPath() ctx.arc(x, y, radius, 0, Math.PI * 2) ctx.fillStyle = colors[i] ctx.fill() } frame++ // Continue animation if (frame < 200) { setTimeout(animate, 50) } } console.log('Starting canvas animation...') animate()`, category: 'graphics', tags: ['canvas', 'animation', 'graphics', 'intermediate'], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), difficulty: 'intermediate', estimatedRunTime: 10000, isBuiltIn: true, usageCount: 0, rating: 4.8 }, { id: 'js-data-structures', name: 'Data Structures Demo', description: 'Demonstrate common data structures and algorithms', language: 'javascript', code: `// Stack implementation class Stack { constructor() { this.items = [] } push(item) { this.items.push(item) } pop() { return this.items.pop() } peek() { return this.items[this.items.length - 1] } isEmpty() { return this.items.length === 0 } } // Queue implementation class Queue { constructor() { this.items = [] } enqueue(item) { this.items.push(item) } dequeue() { return this.items.shift() } front() { return this.items[0] } isEmpty() { return this.items.length === 0 } } // Demonstrate usage console.log('=== Stack Demo ===') const stack = new Stack() stack.push(1) stack.push(2) stack.push(3) console.log('Stack after pushes:', stack.items) console.log('Popped:', stack.pop()) console.log('Stack after pop:', stack.items) console.log('\\n=== Queue Demo ===') const queue = new Queue() queue.enqueue('A') queue.enqueue('B') queue.enqueue('C') console.log('Queue after enqueues:', queue.items) console.log('Dequeued:', queue.dequeue()) console.log('Queue after dequeue:', queue.items) // Binary search function binarySearch(arr, target) { let left = 0 let right = arr.length - 1 while (left <= right) { const mid = Math.floor((left + right) / 2) if (arr[mid] === target) { return mid } else if (arr[mid] < target) { left = mid + 1 } else { right = mid - 1 } } return -1 } console.log('\\n=== Binary Search Demo ===') const sortedArray = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] console.log('Array:', sortedArray) console.log('Search for 7:', binarySearch(sortedArray, 7)) console.log('Search for 12:', binarySearch(sortedArray, 12))`, category: 'algorithms', tags: ['data-structures', 'algorithms', 'stack', 'queue', 'search', 'advanced'], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), difficulty: 'advanced', estimatedRunTime: 200, isBuiltIn: true, usageCount: 0, rating: 4.7 }, // Python Templates { id: 'py-hello-world', name: 'Hello World', description: 'Basic Hello World example in Python', language: 'python', code: `print("Hello, World!") print("Welcome to Python!") # Variables and basic operations name = "Python" version = 3.9 print(f"Language: {name}, Version: {version}") # List operations numbers = [1, 2, 3, 4, 5] print(f"Numbers: {numbers}") print(f"Sum: {sum(numbers)}") print(f"Max: {max(numbers)}")`, category: 'basics', tags: ['beginner', 'print', 'variables', 'lists'], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), difficulty: 'beginner', estimatedRunTime: 100, isBuiltIn: true, usageCount: 0, rating: 5, requirements: [] }, { id: 'py-data-analysis', name: 'Data Analysis with Pandas', description: 'Basic data analysis using pandas and numpy', language: 'python', code: `import pandas as pd import numpy as np # Create sample data data = { 'Name': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'], 'Age': [25, 30, 35, 28, 32], 'City': ['New York', 'London', 'Paris', 'Tokyo', 'Sydney'], 'Salary': [50000, 60000, 70000, 55000, 65000] } # Create DataFrame df = pd.DataFrame(data) print("Original DataFrame:") print(df) print() # Basic statistics print("Basic Statistics:") print(df.describe()) print() # Data filtering print("People over 30:") print(df[df['Age'] > 30]) print() # Data aggregation print("Average salary by city:") city_avg = df.groupby('City')['Salary'].mean() print(city_avg) print() # Adding a new column df['Salary_USD'] = df['Salary'] df['Salary_EUR'] = df['Salary'] * 0.85 # Example conversion print("DataFrame with new columns:") print(df[['Name', 'Salary_USD', 'Salary_EUR']])`, category: 'data-science', tags: ['pandas', 'numpy', 'data-analysis', 'intermediate'], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), difficulty: 'intermediate', estimatedRunTime: 500, requirements: ['pandas', 'numpy'], isBuiltIn: true, usageCount: 0, rating: 4.6 }, { id: 'py-matplotlib-plots', name: 'Data Visualization with Matplotlib', description: 'Create various types of plots using matplotlib', language: 'python', code: `import matplotlib.pyplot as plt import numpy as np # Create sample data x = np.linspace(0, 10, 100) y1 = np.sin(x) y2 = np.cos(x) y3 = np.sin(x) * np.cos(x) # Create the plot plt.figure(figsize=(10, 6)) # Plot multiple lines plt.plot(x, y1, 'b-', label='sin(x)', linewidth=2) plt.plot(x, y2, 'r--', label='cos(x)', linewidth=2) plt.plot(x, y3, 'g:', label='sin(x)*cos(x)', linewidth=2) # Customize the plot plt.title('Trigonometric Functions', fontsize=16, fontweight='bold') plt.xlabel('x', fontsize=12) plt.ylabel('y', fontsize=12) plt.legend(fontsize=10) plt.grid(True, alpha=0.3) # Add some styling plt.tight_layout() plt.show() print("Matplotlib plot created successfully!") print("The plot shows three trigonometric functions:") print("- Blue solid line: sin(x)") print("- Red dashed line: cos(x)") print("- Green dotted line: sin(x)*cos(x)")`, category: 'visualization', tags: ['matplotlib', 'visualization', 'plots', 'numpy', 'intermediate'], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), difficulty: 'intermediate', estimatedRunTime: 800, requirements: ['matplotlib', 'numpy'], isBuiltIn: true, usageCount: 0, rating: 4.8 }, { id: 'py-machine-learning', name: 'Simple Machine Learning', description: 'Basic machine learning example with scikit-learn', language: 'python', code: `import numpy as np from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score import matplotlib.pyplot as plt # Generate sample data np.random.seed(42) X = np.random.rand(100, 1) * 10 # Features y = 2 * X.ravel() + 1 + np.random.randn(100) * 2 # Target with noise print("Generated dataset:") print(f"Features shape: {X.shape}") print(f"Target shape: {y.shape}") print(f"Feature range: [{X.min():.2f}, {X.max():.2f}]") print(f"Target range: [{y.min():.2f}, {y.max():.2f}]") print() # Split the data X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # Create and train the model model = LinearRegression() model.fit(X_train, y_train) # Make predictions y_pred = model.predict(X_test) # Calculate metrics mse = mean_squared_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print("Model Performance:") print(f"Mean Squared Error: {mse:.2f}") print(f"R² Score: {r2:.2f}") print(f"Model coefficients: {model.coef_[0]:.2f}") print(f"Model intercept: {model.intercept_:.2f}") print() # Visualize results plt.figure(figsize=(10, 6)) plt.scatter(X_test, y_test, color='blue', label='Actual', alpha=0.7) plt.scatter(X_test, y_pred, color='red', label='Predicted', alpha=0.7) plt.plot(X_test, y_pred, color='red', linewidth=2) plt.xlabel('Feature') plt.ylabel('Target') plt.title('Linear Regression: Actual vs Predicted') plt.legend() plt.grid(True, alpha=0.3) plt.show() print("Machine learning model trained and visualized successfully!")`, category: 'machine-learning', tags: ['scikit-learn', 'machine-learning', 'regression', 'matplotlib', 'advanced'], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), difficulty: 'advanced', estimatedRunTime: 1500, requirements: ['scikit-learn', 'matplotlib', 'numpy'], isBuiltIn: true, usageCount: 0, rating: 4.9 } ] this.templates.push(...builtInTemplates) this.initializeCategories() } /** * Initialize template categories */ private initializeCategories(): void { this.categories.push( { id: 'basics', name: 'Basics', description: 'Fundamental programming concepts', icon: 'ri:book-line', color: '#4ECDC4', templates: this.templates.filter(t => t.category === 'basics') }, { id: 'async', name: 'Async Programming', description: 'Asynchronous programming patterns', icon: 'ri:time-line', color: '#45B7D1', templates: this.templates.filter(t => t.category === 'async') }, { id: 'graphics', name: 'Graphics & Animation', description: 'Canvas graphics and animations', icon: 'ri:palette-line', color: '#FF6B6B', templates: this.templates.filter(t => t.category === 'graphics') }, { id: 'algorithms', name: 'Algorithms', description: 'Data structures and algorithms', icon: 'ri:mind-map', color: '#96CEB4', templates: this.templates.filter(t => t.category === 'algorithms') }, { id: 'data-science', name: 'Data Science', description: 'Data analysis and manipulation', icon: 'ri:bar-chart-line', color: '#FFEAA7', templates: this.templates.filter(t => t.category === 'data-science') }, { id: 'visualization', name: 'Data Visualization', description: 'Charts and plots', icon: 'ri:line-chart-line', color: '#DDA0DD', templates: this.templates.filter(t => t.category === 'visualization') }, { id: 'machine-learning', name: 'Machine Learning', description: 'ML algorithms and models', icon: 'ri:brain-line', color: '#FFA07A', templates: this.templates.filter(t => t.category === 'machine-learning') } ) } /** * Get all templates */ getTemplates(): CodeTemplate[] { return [...this.templates] } /** * Get templates by category */ getTemplatesByCategory(categoryId: string): CodeTemplate[] { return this.templates.filter(t => t.category === categoryId) } /** * Get templates by language */ getTemplatesByLanguage(language: string): CodeTemplate[] { return this.templates.filter(t => t.language.toLowerCase() === language.toLowerCase()) } /** * Get template by ID */ getTemplate(id: string): CodeTemplate | undefined { return this.templates.find(t => t.id === id) } /** * Search templates */ searchTemplates(query: string, filters?: { language?: string category?: string difficulty?: string tags?: string[] }): CodeTemplate[] { let results = this.templates // Text search if (query.trim()) { const searchTerm = query.toLowerCase() results = results.filter(template => template.name.toLowerCase().includes(searchTerm) || template.description.toLowerCase().includes(searchTerm) || template.tags.some(tag => tag.toLowerCase().includes(searchTerm)) || template.code.toLowerCase().includes(searchTerm) ) } // Apply filters if (filters) { if (filters.language) { results = results.filter(t => t.language.toLowerCase() === filters.language!.toLowerCase()) } if (filters.category) { results = results.filter(t => t.category === filters.category) } if (filters.difficulty) { results = results.filter(t => t.difficulty === filters.difficulty) } if (filters.tags && filters.tags.length > 0) { results = results.filter(t => filters.tags!.some(tag => t.tags.includes(tag)) ) } } return results } /** * Get all categories */ getCategories(): TemplateCategory[] { // Update template counts this.categories.forEach(category => { category.templates = this.templates.filter(t => t.category === category.id) }) return [...this.categories] } /** * Get popular templates */ getPopularTemplates(limit = 10): CodeTemplate[] { return this.templates .sort((a, b) => b.usageCount - a.usageCount) .slice(0, limit) } /** * Get recent templates */ getRecentTemplates(limit = 10): CodeTemplate[] { return this.templates .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) .slice(0, limit) } /** * Add a new template */ addTemplate(template: Omit): string { const id = this.generateId() const now = new Date().toISOString() const newTemplate: CodeTemplate = { id, ...template, createdAt: now, updatedAt: now, isBuiltIn: false, usageCount: 0 } this.templates.push(newTemplate) this.saveUserTemplates() return id } /** * Update a template */ updateTemplate(id: string, updates: Partial): boolean { const template = this.templates.find(t => t.id === id) if (!template || template.isBuiltIn) { return false } Object.assign(template, updates, { updatedAt: new Date().toISOString() }) this.saveUserTemplates() return true } /** * Delete a template */ deleteTemplate(id: string): boolean { const index = this.templates.findIndex(t => t.id === id) if (index === -1 || this.templates[index].isBuiltIn) { return false } this.templates.splice(index, 1) this.saveUserTemplates() return true } /** * Increment template usage count */ incrementUsage(id: string): void { const template = this.templates.find(t => t.id === id) if (template) { template.usageCount++ this.saveUserTemplates() } } /** * Rate a template */ rateTemplate(id: string, rating: number): boolean { const template = this.templates.find(t => t.id === id) if (!template || rating < 1 || rating > 5) { return false } template.rating = rating this.saveUserTemplates() return true } /** * Export templates */ exportTemplates(): string { const userTemplates = this.templates.filter(t => !t.isBuiltIn) return JSON.stringify(userTemplates, null, 2) } /** * Import templates */ importTemplates(jsonData: string): boolean { try { const importedTemplates = JSON.parse(jsonData) as CodeTemplate[] // Validate imported data if (!Array.isArray(importedTemplates)) { throw new Error('Invalid format: expected array') } // Add imported templates importedTemplates.forEach(template => { const existingTemplate = this.templates.find(t => t.id === template.id) if (!existingTemplate) { template.isBuiltIn = false template.usageCount = template.usageCount || 0 this.templates.push(template) } }) this.saveUserTemplates() return true } catch (error) { console.error('Failed to import templates:', error) return false } } private generateId(): string { return `template_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` } private saveUserTemplates(): void { try { const userTemplates = this.templates.filter(t => !t.isBuiltIn) localStorage.setItem(this.userTemplatesKey, JSON.stringify(userTemplates)) } catch (error) { console.warn('Failed to save user templates:', error) } } private loadUserTemplates(): void { try { const stored = localStorage.getItem(this.userTemplatesKey) if (stored) { const userTemplates = JSON.parse(stored) as CodeTemplate[] this.templates.push(...userTemplates) } } catch (error) { console.warn('Failed to load user templates:', error) } } } // Export singleton instance export const codeTemplates = CodeTemplatesService.getInstance() // Export composable for Vue components export function useCodeTemplates() { return { templates: computed(() => codeTemplates.getTemplates()), categories: computed(() => codeTemplates.getCategories()), popularTemplates: computed(() => codeTemplates.getPopularTemplates()), recentTemplates: computed(() => codeTemplates.getRecentTemplates()), getTemplate: codeTemplates.getTemplate.bind(codeTemplates), getTemplatesByCategory: codeTemplates.getTemplatesByCategory.bind(codeTemplates), getTemplatesByLanguage: codeTemplates.getTemplatesByLanguage.bind(codeTemplates), searchTemplates: codeTemplates.searchTemplates.bind(codeTemplates), addTemplate: codeTemplates.addTemplate.bind(codeTemplates), updateTemplate: codeTemplates.updateTemplate.bind(codeTemplates), deleteTemplate: codeTemplates.deleteTemplate.bind(codeTemplates), incrementUsage: codeTemplates.incrementUsage.bind(codeTemplates), rateTemplate: codeTemplates.rateTemplate.bind(codeTemplates), exportTemplates: codeTemplates.exportTemplates.bind(codeTemplates), importTemplates: codeTemplates.importTemplates.bind(codeTemplates) } } ================================================ FILE: web/src/store/index.ts ================================================ import type { App } from 'vue' import { createPinia } from 'pinia' export const store = createPinia() export function setupStore(app: App) { app.use(store) } export * from './modules' ================================================ FILE: web/src/store/modules/app/helper.ts ================================================ import { ss } from '@/utils/storage' const LOCAL_NAME = 'appSetting' export type Theme = 'light' | 'dark' | 'auto' export type Language = 'zh-CN' | 'zh-TW' | 'en-US' export interface AppState { siderCollapsed: boolean theme: Theme language: Language } const defaultLanguage = navigator.language as Language export function defaultSetting(): AppState { return { siderCollapsed: false, theme: 'light', language: defaultLanguage } } export function getLocalSetting(): AppState { const localSetting: AppState | undefined = ss.get(LOCAL_NAME) return { ...defaultSetting(), ...localSetting } } export function setLocalSetting(setting: AppState): void { ss.set(LOCAL_NAME, setting) } ================================================ FILE: web/src/store/modules/app/index.ts ================================================ import { defineStore } from 'pinia' import type { AppState, Language, Theme } from './helper' import { getLocalSetting, setLocalSetting } from './helper' import { store } from '@/store' const langs: Language[] = ['zh-CN', 'en-US', 'zh-CN'] export const useAppStore = defineStore('app-store', { state: (): AppState => getLocalSetting(), actions: { setSiderCollapsed(collapsed: boolean) { this.siderCollapsed = collapsed this.recordState() }, setTheme(theme: Theme) { this.theme = theme this.recordState() }, setLanguage(language: Language) { if (this.language !== language) { this.language = language this.recordState() } }, setNextLanguage() { const currentLang = this.language const nextLang = langs[(langs.indexOf(currentLang) + 1) % langs.length] this.language = nextLang this.recordState() }, recordState() { setLocalSetting(this.$state) }, }, }) export function useAppStoreWithOut() { return useAppStore(store) } ================================================ FILE: web/src/store/modules/auth/helper.ts ================================================ // Hybrid token approach: // - Access tokens: In-memory (short-lived, secure from XSS) // - Refresh tokens: httpOnly cookies (long-lived, persistent) let accessToken: string | null = null export function getToken(): string | null { return accessToken } export function setToken(token: string): void { accessToken = token } export function removeToken(): void { accessToken = null } // Expiration tracking can still be useful for UI state const EXPIRE_LOCAL_NAME = 'expiresIn' export function getExpiresIn(): number | null { const stored = window.localStorage.getItem(EXPIRE_LOCAL_NAME) return stored ? parseInt(stored, 10) : null } export function setExpiresIn(expiresIn: number): void { window.localStorage.setItem(EXPIRE_LOCAL_NAME, expiresIn.toString()) } export function removeExpiresIn(): void { window.localStorage.removeItem(EXPIRE_LOCAL_NAME) } ================================================ FILE: web/src/store/modules/auth/index.ts ================================================ import { defineStore } from 'pinia' import { watch } from 'vue' import { getExpiresIn, getToken, removeExpiresIn, removeToken, setExpiresIn, setToken } from './helper' let activeRefreshPromise: Promise | null = null export interface AuthState { token: string | null // Access token stored in memory expiresIn: number | null isRefreshing: boolean // Track if token refresh is in progress isInitialized: boolean // Track if auth state has been initialized isInitializing: boolean // Track if auth initialization is in progress } export const useAuthStore = defineStore('auth-store', { state: (): AuthState => ({ token: getToken(), // Load token normally expiresIn: getExpiresIn(), isRefreshing: false, isInitialized: false, isInitializing: false, }), getters: { isValid(): boolean { return !!(this.token && this.expiresIn && this.expiresIn > Date.now() / 1000) }, getToken(): string | null { return this.token }, getExpiresIn(): number | null { return this.expiresIn }, needsRefresh(): boolean { // Check if token expires within next 5 minutes const fiveMinutesFromNow = Date.now() / 1000 + 300 return !!(this.expiresIn && this.expiresIn < fiveMinutesFromNow) }, needPermission(): boolean { return this.isInitialized && !this.isInitializing && !this.isValid } }, actions: { async initializeAuth() { if (this.isInitialized || this.isInitializing) return this.isInitializing = true try { const now = Date.now() / 1000 if (this.expiresIn) { const tokenMissing = !this.token const expired = this.expiresIn <= now if (tokenMissing || expired || this.needsRefresh) { console.log('Token expired or about to expire, refreshing...') await this.refreshToken() } } else if (this.token) { // Clear expired token this.removeToken() this.removeExpiresIn() } } catch (error) { // Clear invalid state on error this.removeToken() this.removeExpiresIn() } finally { this.isInitializing = false this.isInitialized = true } }, setToken(token: string) { this.token = token setToken(token) }, removeToken() { this.token = null removeToken() }, async refreshToken() { if (this.isRefreshing && activeRefreshPromise) return activeRefreshPromise console.log('Starting token refresh...') this.isRefreshing = true const refreshOperation = (async () => { try { // Call refresh endpoint - refresh token is sent automatically via httpOnly cookie const response = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include', // Include httpOnly cookies }) console.log('Refresh response status:', response.status) if (response.ok) { const data = await response.json() console.log('Token refresh successful, setting new token') this.setToken(data.accessToken) this.setExpiresIn(data.expiresIn) return data.accessToken as string } // Refresh failed - user needs to login again console.log('Token refresh failed, removing tokens') this.removeToken() this.removeExpiresIn() throw new Error('Token refresh failed') } catch (error) { console.error('Token refresh error:', error) this.removeToken() this.removeExpiresIn() throw error } finally { this.isRefreshing = false activeRefreshPromise = null } })() activeRefreshPromise = refreshOperation return refreshOperation }, setExpiresIn(expiresIn: number) { this.expiresIn = expiresIn setExpiresIn(expiresIn) }, removeExpiresIn() { this.expiresIn = null removeExpiresIn() }, async waitForInitialization(timeoutMs = 10000) { if (!this.isInitializing) { return } await new Promise((resolve) => { let stopWatcher: (() => void) | null = null const timeoutId = setTimeout(() => { if (stopWatcher) stopWatcher() resolve() }, timeoutMs) stopWatcher = watch( () => this.isInitializing, (isInit) => { if (!isInit) { clearTimeout(timeoutId) if (stopWatcher) stopWatcher() resolve() } }, { immediate: false } ) }) }, }, }) ================================================ FILE: web/src/store/modules/index.ts ================================================ export * from './app' export * from './user' export * from './auth' export * from './prompt' export * from './workspace' export * from './session' export * from './message' ================================================ FILE: web/src/store/modules/message/index.ts ================================================ import { defineStore } from 'pinia' import { getChatMessagesBySessionUUID, clearSessionChatMessages, generateMoreSuggestions, } from '@/api' import { deleteChatData } from '@/api' import { createChatPrompt } from '@/api/chat_prompt' import { getDefaultSystemPrompt } from '@/constants/chat' import { nowISO } from '@/utils/date' import { v7 as uuidv7 } from 'uuid' import { useSessionStore } from '../session' export interface MessageState { chat: Record // sessionUuid -> messages isLoading: Record // sessionUuid -> isLoading } export const useMessageStore = defineStore('message-store', { state: (): MessageState => ({ chat: {}, isLoading: {}, }), getters: { getChatSessionDataByUuid(state) { return (uuid?: string) => { if (uuid) { return state.chat[uuid] || [] } return [] } }, getIsLoadingBySession(state) { return (sessionUuid: string) => { return state.isLoading[sessionUuid] || false } }, // Get last message for a session getLastMessageForSession(state) { return (sessionUuid: string) => { const messages = state.chat[sessionUuid] || [] return messages[messages.length - 1] || null } }, // Get all messages for active session activeSessionMessages(state) { const sessionStore = useSessionStore() if (sessionStore.activeSessionUuid) { return state.chat[sessionStore.activeSessionUuid] || [] } return [] }, }, actions: { async syncChatMessages(sessionUuid: string) { if (!sessionUuid) { return } this.isLoading[sessionUuid] = true try { const messageData = await getChatMessagesBySessionUUID(sessionUuid) const normalizedMessages = Array.isArray(messageData) ? messageData : [] // Initialize batching structure for messages with suggested questions const processedMessageData = normalizedMessages.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 }) const hasSystemPrompt = processedMessageData.some((message: Chat.Message) => message.isPrompt) if (!hasSystemPrompt) { try { const defaultPrompt = getDefaultSystemPrompt() const prompt = await createChatPrompt({ uuid: uuidv7(), chatSessionUuid: sessionUuid, role: 'system', content: defaultPrompt, tokenCount: Math.max(1, Math.ceil(defaultPrompt.length / 4)), userId: 0, createdBy: 0, updatedBy: 0, }) const promptMessage: Chat.Message = { uuid: prompt.uuid, dateTime: prompt.updatedAt || nowISO(), text: prompt.content || defaultPrompt, inversion: true, error: false, loading: false, isPrompt: true, } const existingPromptIndex = processedMessageData.findIndex( (message: Chat.Message) => message.uuid === promptMessage.uuid || message.isPrompt, ) if (existingPromptIndex === -1) { processedMessageData.unshift(promptMessage) } else { processedMessageData[existingPromptIndex] = { ...processedMessageData[existingPromptIndex], ...promptMessage, isPrompt: true, } } } catch (error) { console.error(`Failed to create default system prompt for session ${sessionUuid}:`, error) } } this.chat[sessionUuid] = processedMessageData // Update active session if needed const sessionStore = useSessionStore() if (sessionStore.activeSessionUuid !== sessionUuid) { const session = sessionStore.getChatSessionByUuid(sessionUuid) if (session?.workspaceUuid) { await sessionStore.setActiveSession(session.workspaceUuid, sessionUuid) } } return processedMessageData } catch (error) { console.error(`Failed to sync messages for session ${sessionUuid}:`, error) throw error } finally { this.isLoading[sessionUuid] = false } }, addMessage(sessionUuid: string, message: Chat.Message) { if (!this.chat[sessionUuid]) { this.chat[sessionUuid] = [] } this.chat[sessionUuid].push(message) }, addMessages(sessionUuid: string, messages: Chat.Message[]) { if (!this.chat[sessionUuid]) { this.chat[sessionUuid] = [] } this.chat[sessionUuid].push(...messages) }, updateMessage(sessionUuid: string, messageUuid: string, updates: Partial) { const messages = this.chat[sessionUuid] if (messages) { const index = messages.findIndex(msg => msg.uuid === messageUuid) if (index !== -1) { messages[index] = { ...messages[index], ...updates } } } }, async removeMessage(sessionUuid: string, messageUuid: string) { try { const message = this.chat[sessionUuid]?.find(msg => msg.uuid === messageUuid) if (!message) { return } // Call the API to delete the message from the server await deleteChatData(message) // Remove the message from local state after successful API call if (this.chat[sessionUuid]) { this.chat[sessionUuid] = this.chat[sessionUuid].filter( msg => msg.uuid !== messageUuid ) } } catch (error) { console.error(`Failed to delete message ${messageUuid}:`, error) throw error } }, clearSessionMessages(sessionUuid: string) { try { clearSessionChatMessages(sessionUuid) // Keep the first message (system prompt) and clear the rest const messages = this.chat[sessionUuid] || [] if (messages.length > 0) { this.chat[sessionUuid] = [messages[0]] // Keep only the first message } else { this.chat[sessionUuid] = [] } } catch (error) { console.error(`Failed to clear messages for session ${sessionUuid}:`, error) throw error } }, updateLastMessage(sessionUuid: string, updates: Partial) { const messages = this.chat[sessionUuid] if (messages && messages.length > 0) { const lastIndex = messages.length - 1 messages[lastIndex] = { ...messages[lastIndex], ...updates } } }, // Helper method to set loading state setLoading(sessionUuid: string, isLoading: boolean) { this.isLoading[sessionUuid] = isLoading }, // Helper method to get message count for a session getMessageCount(sessionUuid: string) { return this.chat[sessionUuid]?.length || 0 }, // Helper method to clear all messages clearAllMessages() { this.chat = {} this.isLoading = {} }, // Helper method to remove session data removeSessionData(sessionUuid: string) { delete this.chat[sessionUuid] delete this.isLoading[sessionUuid] }, // Helper method to check if session has messages hasMessages(sessionUuid: string) { return this.chat[sessionUuid]?.length > 0 }, // Helper method to get messages by type getMessagesByType(sessionUuid: string, type: 'user' | 'assistant') { const messages = this.chat[sessionUuid] || [] return messages.filter(msg => { if (type === 'user') { return msg.inversion } return !msg.inversion }) }, // Helper method to get pinned messages getPinnedMessages(sessionUuid: string) { const messages = this.chat[sessionUuid] || [] return messages.filter(msg => msg.isPin) }, // Helper method to get messages with artifacts getMessagesWithArtifacts(sessionUuid: string) { const messages = this.chat[sessionUuid] || [] return messages.filter(msg => msg.artifacts && msg.artifacts.length > 0) }, // Helper method to get messages by date range getMessagesByDateRange(sessionUuid: string, startDate: string, endDate: string) { const messages = this.chat[sessionUuid] || [] return messages.filter(msg => { const messageDate = new Date(msg.dateTime) return messageDate >= new Date(startDate) && messageDate <= new Date(endDate) }) }, // Helper method to search messages searchMessages(sessionUuid: string, query: string) { const messages = this.chat[sessionUuid] || [] const lowercaseQuery = query.toLowerCase() return messages.filter(msg => msg.text.toLowerCase().includes(lowercaseQuery) ) }, // Helper method to get loading messages for a session getLoadingMessages(sessionUuid: string) { const messages = this.chat[sessionUuid] || [] return messages.filter(msg => msg.loading) }, // Helper method to get error messages for a session getErrorMessages(sessionUuid: string) { const messages = this.chat[sessionUuid] || [] return messages.filter(msg => msg.error) }, // Helper method to get prompt messages for a session getPromptMessages(sessionUuid: string) { 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, }) } } }, }, }) ================================================ FILE: web/src/store/modules/prompt/helper.ts ================================================ import { ss } from '@/utils/storage' const LOCAL_NAME = 'promptStore' export type Prompt = { key: string value: string } export type PromptList = [] | Prompt[] export interface PromptStore { promptList: PromptList } const defaultChinesePromptList: PromptList = [ { "key": "充当英语翻译和改进者", "value": "我希望你能担任英语翻译、拼写校对和修辞改进的角色。我会用任何语言和你交流,你会识别语言,将其翻译并用更为优美和精炼的英语回答我。请将我简单的词汇和句子替换成更为优美和高雅的表达方式,确保意思不变,但使其更具文学性。请仅回答更正和改进的部分,不要写解释。我的第一句话是“how are you ?”,请翻译它。" }, { "key": "充当英翻中", "value": "下面我让你来充当翻译家,你的目标是把任何语言翻译成中文,请翻译时不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面这句话:“how are you ?”" }, { "key": "充当英文词典(附中文解释)", "value": "我想让你充当英文词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈,第一个单词是“Hello\"" }, { "key": "充当讲故事的人", "value": "我想让你扮演讲故事的角色。您将想出引人入胜、富有想象力和吸引观众的有趣故事。它可以是童话故事、教育故事或任何其他类型的故事,有可能吸引人们的注意力和想象力。根据目标受众,您可以为讲故事环节选择特定的主题或主题,例如,如果是儿童,则可以谈论动物;如果是成年人,那么基于历史的故事可能会更好地吸引他们等等。我的第一个要求是“我需要一个关于毅力的有趣故事。”" }, { "key": "充当 AI 辅助医生", "value": "我想让你扮演一名人工智能辅助医生。我将为您提供患者的详细信息,您的任务是使用最新的人工智能工具,例如医学成像软件和其他机器学习程序,以诊断最可能导致其症状的原因。您还应该将体检、实验室测试等传统方法纳入您的评估过程,以确保准确性。我的第一个请求是“我需要帮助诊断一例严重的腹痛”。" }, { "key": "充当医生", "value": "我想让你扮演医生的角色,想出创造性的治疗方法来治疗疾病。您应该能够推荐常规药物、草药和其他天然替代品。在提供建议时,您还需要考虑患者的年龄、生活方式和病史。我的第一个建议请求是“为患有关节炎的老年患者提出一个侧重于整体治疗方法的治疗计划”。" }, { "key": "担任会计师", "value": "我希望你担任会计师,并想出创造性的方法来管理财务。在为客户制定财务计划时,您需要考虑预算、投资策略和风险管理。在某些情况下,您可能还需要提供有关税收法律法规的建议,以帮助他们实现利润最大化。我的第一个建议请求是“为小型企业制定一个专注于成本节约和长期投资的财务计划”。" }, { "key": "担任厨师", "value": "我需要有人可以推荐美味的食谱,这些食谱包括营养有益但又简单又不费时的食物,因此适合像我们这样忙碌的人以及成本效益等其他因素,因此整体菜肴最终既健康又经济!我的第一个要求——“一些清淡而充实的东西,可以在午休时间快速煮熟”" }, { "key": "充当自助书", "value": "我要你充当一本自助书。您会就如何改善我生活的某些方面(例如人际关系、职业发展或财务规划)向我提供建议和技巧。例如,如果我在与另一半的关系中挣扎,你可以建议有用的沟通技巧,让我们更亲近。我的第一个请求是“我需要帮助在困难时期保持积极性”。" }, { "key": "充当格言书", "value": "我要你充当格言书。您将为我提供明智的建议、鼓舞人心的名言和意味深长的名言,以帮助指导我的日常决策。此外,如有必要,您可以提出将此建议付诸行动或其他相关主题的实用方法。我的第一个请求是“我需要关于如何在逆境中保持积极性的指导”。" }, { "key": "充当心理学家", "value": "我想让你扮演一个心理学家。我会告诉你我的想法。我希望你能给我科学的建议,让我感觉更好。我的第一个想法,{ 在这里输入你的想法,如果你解释得更详细,我想你会得到更准确的答案。}" }, { "key": "充当个人购物员", "value": "我想让你做我的私人采购员。我会告诉你我的预算和喜好,你会建议我购买的物品。您应该只回复您推荐的项目,而不是其他任何内容。不要写解释。我的第一个请求是“我有 100 美元的预算,我正在寻找一件新衣服。”" }, { "key": "充当美食评论家", "value": "我想让你扮演美食评论家。我会告诉你一家餐馆,你会提供对食物和服务的评论。您应该只回复您的评论,而不是其他任何内容。不要写解释。我的第一个请求是“我昨晚去了一家新的意大利餐厅。你能提供评论吗?”" }, { "key": "充当虚拟医生", "value": "我想让你扮演虚拟医生。我会描述我的症状,你会提供诊断和治疗方案。只回复你的诊疗方案,其他不回复。不要写解释。我的第一个请求是“最近几天我一直感到头痛和头晕”。" }, { "key": "担任私人厨师", "value": "我要你做我的私人厨师。我会告诉你我的饮食偏好和过敏,你会建议我尝试的食谱。你应该只回复你推荐的食谱,别无其他。不要写解释。我的第一个请求是“我是一名素食主义者,我正在寻找健康的晚餐点子。”" }, { "key": "担任法律顾问", "value": "我想让你做我的法律顾问。我将描述一种法律情况,您将就如何处理它提供建议。你应该只回复你的建议,而不是其他。不要写解释。我的第一个请求是“我出了车祸,不知道该怎么办”。" }, { "key": "作为个人造型师", "value": "我想让你做我的私人造型师。我会告诉你我的时尚偏好和体型,你会建议我穿的衣服。你应该只回复你推荐的服装,别无其他。不要写解释。我的第一个请求是“我有一个正式的活动要举行,我需要帮助选择一套衣服。”" }, { "key": "担任语言病理学家 (SLP)", "value": "我希望你扮演一名言语语言病理学家 (SLP),想出新的言语模式、沟通策略,并培养对他们不口吃的沟通能力的信心。您应该能够推荐技术、策略和其他治疗方法。在提供建议时,您还需要考虑患者的年龄、生活方式和顾虑。我的第一个建议要求是“为一位患有口吃和自信地与他人交流有困难的年轻成年男性制定一个治疗计划”" }, { "key": "担任创业技术律师", "value": "我将要求您准备一页纸的设计合作伙伴协议草案,该协议是一家拥有 IP 的技术初创公司与该初创公司技术的潜在客户之间的协议,该客户为该初创公司正在解决的问题空间提供数据和领域专业知识。您将写下大约 1 a4 页的拟议设计合作伙伴协议,涵盖 IP、机密性、商业权利、提供的数据、数据的使用等所有重要方面。" }, { "key": "充当书面作品的标题生成器", "value": "我想让你充当书面作品的标题生成器。我会给你提供一篇文章的主题和关键词,你会生成五个吸引眼球的标题。请保持标题简洁,不超过 20 个字,并确保保持意思。回复将使用主题的语言类型。我的第一个主题是“LearnData,一个建立在 VuePress 上的知识库,里面整合了我所有的笔记和文章,方便我使用和分享。”" }, { "key": "扮演醉汉", "value": "我要你扮演一个喝醉的人。您只会像一个喝醉了的人发短信一样回答,仅此而已。你的醉酒程度会在你的答案中故意和随机地犯很多语法和拼写错误。你也会随机地忽略我说的话,并随机说一些与我提到的相同程度的醉酒。不要在回复上写解释。我的第一句话是“你好吗?”" }, { "key": "担任数学历史老师", "value": "我想让你充当数学历史老师,提供有关数学概念的历史发展和不同数学家的贡献的信息。你应该只提供信息而不是解决数学问题。使用以下格式回答:“{数学家/概念} - {他们的贡献/发展的简要总结}。我的第一个问题是“毕达哥拉斯对数学的贡献是什么?”" }, ] const defaultEnglishPromptList: PromptList = [ { "key": "Act as an English Translator and Improver", "value": "I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is \"istanbulu cok seviyom burada olmak cok guzel\" and I want you to correct it and improve it. I want you to reply me with the corrected and improved version of my sentence, in English." }, { "key": "Act as a Spoken English Teacher and Improver", "value": "I want you to act as a spoken English teacher and improver. I will speak to you in English and you will reply to me in English to practice my spoken English. I want you to keep your reply neat, limiting the reply to 100 words. I want you to strictly correct my grammar mistakes, typos, and factual errors. I want you to ask me a question in your reply. Now let's start practicing, you could ask me a question first. Remember, I want you to strictly correct my grammar mistakes, typos, and factual errors." }, { "key": "Act as a Poet", "value": "I want you to act as a poet. You will create poems that evoke emotions and have the power to stir people's soul. Write on any topic or theme but make sure your words convey the feeling you are trying to express in beautiful yet meaningful ways. You can also come up with short verses that are still powerful enough to leave an imprint in readers' minds." }, { "key": "Act as a Note Taking Assistant", "value": "I want you to act as a note-taking assistant for a lecture. Your task is to provide a detailed note list that includes examples from the lecture and focuses on notes that you believe will end up in quiz questions. Additionally, please make a separate list for notes that have numbers and data in them and another separated list for the examples that included in this lecture. The notes should be concise and easy to read." }, { "key": "Act as a Career Counselor", "value": "I want you to act as a career counselor. I will provide you with information about my skills, interests, and career goals, and you will suggest career paths that suit me. You should also provide information about required qualifications, potential career progression, and salary ranges. My first request is 'I have a degree in computer science and I enjoy problem-solving and working with data.'" }, { "key": "Act as a Financial Advisor", "value": "I want you to act as a financial advisor. I will provide you with information about my financial situation and goals, and you will suggest ways to manage my money, invest, and save for the future. You should consider factors like risk tolerance, time horizon, and financial obligations. My first request is 'I'm 30 years old, earn $80,000 per year, and want to retire by age 60 with $2 million.'" }, { "key": "Act as a Travel Guide", "value": "I want you to act as a travel guide. I will tell you about a destination I want to visit, and you will suggest places to visit, things to do, local customs to be aware of, and practical travel tips. You should also recommend accommodations and transportation options. My first request is 'I'm planning a trip to Japan for two weeks in spring.'" }, { "key": "Act as a Software Developer", "value": "I want you to act as a software developer. I will provide you with requirements for an application, and you will suggest technologies to use, architecture patterns, and implementation details. You should also provide code snippets when appropriate. My first request is 'I need to build a web application for managing tasks with user authentication.'" }, { "key": "Act as a Personal Trainer", "value": "I want you to act as a personal trainer. I will provide you with information about my fitness level, goals, and available equipment, and you will create a workout plan for me. You should also provide nutrition advice and tips for staying motivated. My first request is 'I'm a beginner who wants to lose weight and tone up. I have access to a gym and basic equipment at home.'" }, { "key": "Act as a Language Learning Partner", "value": "I want you to act as a language learning partner. I will tell you which language I want to learn, and you will engage me in conversation practice, correct my mistakes, and explain grammar concepts. You should also suggest learning resources and techniques. My first request is 'I want to learn Spanish and I'm a complete beginner.'" }, { "key": "Act as a Debate Coach", "value": "I want you to act as a debate coach. I will provide you with a topic, and you will help me develop arguments, counterarguments, and rebuttals. You should also provide tips on delivery and persuasion techniques. My first request is 'The topic is whether social media has had a positive impact on society.'" }, { "key": "Act as a UX/UI Designer", "value": "I want you to act as a UX/UI designer. I will describe a product or feature, and you will suggest user flows, wireframes, and design principles to follow. You should consider usability, accessibility, and visual appeal. My first request is 'I need to design a mobile app for tracking daily water intake.'" } ] let defaultPromptMap: { [key: string]: Prompt[] } = { 'zh-CN': defaultChinesePromptList, 'zh-TW': defaultChinesePromptList, 'en-US': defaultEnglishPromptList, } export function getLocalPromptList(): PromptStore { const promptStore: PromptStore | undefined = ss.get(LOCAL_NAME) if (promptStore && promptStore?.promptList?.length > 0) { return promptStore } else { let defaultPromptList = defaultPromptMap[navigator.language]; setLocalPromptList({ promptList: defaultPromptList }) return { promptList: defaultPromptList } } } export function setLocalPromptList(promptStore: PromptStore): void { ss.set(LOCAL_NAME, promptStore) } ================================================ FILE: web/src/store/modules/prompt/index.ts ================================================ import { defineStore } from 'pinia' import type { PromptStore } from './helper' import { getLocalPromptList, setLocalPromptList } from './helper' export const usePromptStore = defineStore('prompt-store', { state: (): PromptStore => getLocalPromptList(), actions: { updatePromptList(promptList: []) { this.$patch({ promptList }) setLocalPromptList({ promptList }) }, getPromptList() { return this.$state }, }, }) ================================================ FILE: web/src/store/modules/session/index.ts ================================================ import { defineStore } from 'pinia' import { router } from '@/router' import { createChatSession, deleteChatSession, renameChatSession, updateChatSession, getSessionsByWorkspace, createSessionInWorkspace, } from '@/api' import { getDefaultSystemPrompt } from '@/constants/chat' import { useWorkspaceStore } from '../workspace' export interface SessionState { workspaceHistory: Record // workspaceUuid -> sessions activeSessionUuid: string | null isLoading: boolean isCreatingSession: boolean isSwitchingSession: boolean isNavigating: boolean lastRequestedSessionUuid: string | null // Track the most recent session switch request } export const useSessionStore = defineStore('session-store', { state: (): SessionState => ({ workspaceHistory: {}, activeSessionUuid: null, isLoading: false, isCreatingSession: false, isSwitchingSession: false, isNavigating: false, lastRequestedSessionUuid: null, }), getters: { getChatSessionByUuid(state) { return (uuid?: string) => { if (uuid) { // Search across all workspace histories for (const sessions of Object.values(state.workspaceHistory)) { const session = sessions.find(item => item.uuid === uuid) if (session) return session } } return null } }, getSessionsByWorkspace(state) { return (workspaceUuid?: string) => { if (!workspaceUuid) return [] return state.workspaceHistory[workspaceUuid] || [] } }, activeSession(state) { if (state.activeSessionUuid) { // Search across all workspace histories for (const sessions of Object.values(state.workspaceHistory)) { const session = sessions.find(item => item.uuid === state.activeSessionUuid) if (session) return session } } return null }, // Get session URL for navigation getSessionUrl() { return (sessionUuid: string): string => { // Search across all workspace histories for (const sessions of Object.values(this.workspaceHistory)) { const session = sessions.find(item => item.uuid === sessionUuid) if (session && session.workspaceUuid) { return `/#/workspace/${session.workspaceUuid}/chat/${sessionUuid}` } } return `/#/chat/${sessionUuid}` } }, }, actions: { async syncWorkspaceSessions(workspaceUuid: string) { try { this.isLoading = true const sessions = await getSessionsByWorkspace(workspaceUuid) // Map topic to title for frontend compatibility const sessionsWithTitle = sessions.map((session: any) => ({ ...session, title: session.topic || session.title || 'Untitled' })) this.workspaceHistory[workspaceUuid] = sessionsWithTitle return sessionsWithTitle } catch (error) { console.error(`Failed to sync sessions for workspace ${workspaceUuid}:`, error) throw error } finally { this.isLoading = false } }, // Optimized method to load only active workspace sessions async syncActiveWorkspaceSessions() { try { this.isLoading = true const workspaceStore = useWorkspaceStore() if (!workspaceStore.activeWorkspaceUuid) { console.log('No active workspace, skipping session sync') return } console.log('Loading sessions for active workspace:', workspaceStore.activeWorkspaceUuid) const sessions = await getSessionsByWorkspace(workspaceStore.activeWorkspaceUuid) // Map topic to title for frontend compatibility const sessionsWithTitle = sessions.map((session: any) => ({ ...session, title: session.topic || session.title || 'Untitled' })) this.workspaceHistory[workspaceStore.activeWorkspaceUuid] = sessionsWithTitle console.log(`✅ Loaded ${sessionsWithTitle.length} sessions for active workspace`) return sessionsWithTitle } catch (error) { console.error('Failed to sync active workspace sessions:', error) throw error } finally { this.isLoading = false } }, async syncAllWorkspaceSessions() { try { this.isLoading = true const workspaceStore = useWorkspaceStore() // Sync sessions for all workspaces for (const workspace of workspaceStore.workspaces) { const sessions = await getSessionsByWorkspace(workspace.uuid) // Map topic to title for frontend compatibility const sessionsWithTitle = sessions.map((session: any) => ({ ...session, title: session.topic || session.title || 'Untitled' })) this.workspaceHistory[workspace.uuid] = sessionsWithTitle } } catch (error) { console.error('Failed to sync all workspace sessions:', error) throw error } finally { this.isLoading = false } }, async createSessionInWorkspace(title: string, workspaceUuid?: string, model?: string) { if (this.isCreatingSession) { return null } this.isCreatingSession = true try { const workspaceStore = useWorkspaceStore() const targetWorkspaceUuid = workspaceUuid || workspaceStore.activeWorkspaceUuid if (!targetWorkspaceUuid) { throw new Error('No workspace available for session creation') } // Get default model if none provided let sessionModel = model if (!sessionModel) { try { const { fetchDefaultChatModel } = await import('@/api/chat_model') const defaultModel = await fetchDefaultChatModel() sessionModel = defaultModel.name } catch (error) { console.warn('Failed to fetch default model, proceeding without model:', error) } } const newSession = await createSessionInWorkspace(targetWorkspaceUuid, { topic: title, model: sessionModel, defaultSystemPrompt: getDefaultSystemPrompt(), }) // Map topic to title for frontend compatibility const sessionWithTitle = { ...newSession, title: newSession.topic || title, model: newSession.model || sessionModel } // Add to workspace history if (!this.workspaceHistory[targetWorkspaceUuid]) { this.workspaceHistory[targetWorkspaceUuid] = [] } this.workspaceHistory[targetWorkspaceUuid].unshift(sessionWithTitle) // Set as active session await this.setActiveSession(targetWorkspaceUuid, sessionWithTitle.uuid) return sessionWithTitle } catch (error) { console.error('Failed to create session in workspace:', error) throw error } finally { this.isCreatingSession = false } }, async createLegacySession(session: Chat.Session) { try { await createChatSession(session.uuid, session.title, session.model, getDefaultSystemPrompt()) // Refresh workspace sessions to get updated list from backend const workspaceUuid = session.workspaceUuid if (workspaceUuid) { await this.syncWorkspaceSessions(workspaceUuid) } await this.setActiveSession(workspaceUuid || null, session.uuid) return session } catch (error) { console.error('Failed to create legacy session:', error) throw error } }, async updateSession(uuid: string, updates: Partial) { try { console.log('updateSession called with uuid:', uuid, 'updates:', updates) console.log('Current workspaceHistory:', this.workspaceHistory) // Find session across all workspace histories for (const workspaceUuid in this.workspaceHistory) { const sessions = this.workspaceHistory[workspaceUuid] const index = sessions.findIndex(item => item.uuid === uuid) if (index !== -1) { console.log('Found session in workspace:', workspaceUuid, 'at index:', index) // Update local state sessions[index] = { ...sessions[index], ...updates } // Update backend - use the appropriate API method if (updates.title !== undefined) { // If only title is changing, use the rename endpoint await renameChatSession(uuid, sessions[index].title) } else { // For other updates (like model), use the full update endpoint await updateChatSession(uuid, sessions[index]) } return sessions[index] } } // If session not found locally, try to update it on the backend anyway // This handles cases where the session exists on the server but not in local state console.log('Session not found locally, attempting backend update') try { const session = this.getChatSessionByUuid(uuid) if (session) { console.log('Found session via getter, updating') const updatedSession = { ...session, ...updates } await updateChatSession(uuid, updatedSession) return updatedSession } } catch (backendError) { console.error('Backend update also failed:', backendError) } throw new Error(`Session ${uuid} not found`) } catch (error) { console.error('Failed to update session:', error) throw error } }, async deleteSession(sessionUuid: string) { try { // Find session and its workspace let workspaceUuid: string | null = null for (const [wUuid, sessions] of Object.entries(this.workspaceHistory)) { const index = sessions.findIndex(item => item.uuid === sessionUuid) if (index !== -1) { workspaceUuid = wUuid break } } if (workspaceUuid) { // Remove from workspace history this.workspaceHistory[workspaceUuid] = this.workspaceHistory[workspaceUuid].filter( session => session.uuid !== sessionUuid ) } // Delete from backend await deleteChatSession(sessionUuid) // Clear active session if it was deleted if (this.activeSessionUuid === sessionUuid) { await this.setNextActiveSession(workspaceUuid) } // Clear from workspace active sessions if (workspaceUuid) { const workspaceStore = useWorkspaceStore() workspaceStore.clearActiveSessionForWorkspace(workspaceUuid) } } catch (error) { console.error('Failed to delete session:', error) throw error } }, async setActiveSession(workspaceUuid: string | null, sessionUuid: string) { // Early return if this is already the active session if (this.activeSessionUuid === sessionUuid) { return } // Track this as the most recent requested session this.lastRequestedSessionUuid = sessionUuid // If already switching, wait a bit and check if this request is still the latest if (this.isSwitchingSession) { console.log('Session switch in progress, deferring request for:', sessionUuid) // Wait for current switch to complete await new Promise(resolve => setTimeout(resolve, 100)) // Check if a newer request came in while we were waiting if (this.lastRequestedSessionUuid !== sessionUuid) { console.log('Ignoring stale session switch request for:', sessionUuid) return } } this.isSwitchingSession = true try { // Double-check this is still the latest requested session if (this.lastRequestedSessionUuid !== sessionUuid) { console.log('Aborting session switch, newer request exists:', this.lastRequestedSessionUuid) return } this.activeSessionUuid = sessionUuid // Update workspace active session tracking if (workspaceUuid) { const workspaceStore = useWorkspaceStore() workspaceStore.setActiveSessionForWorkspace(workspaceUuid, sessionUuid) } // Navigate to the session await this.navigateToSession(sessionUuid) } catch (error) { console.error('Failed to set active session:', error) throw error } finally { this.isSwitchingSession = false } }, // Set active session without navigation (for workspace switching) setActiveSessionWithoutNavigation(workspaceUuid: string | null, sessionUuid: string) { this.activeSessionUuid = sessionUuid // Update workspace active session tracking if (workspaceUuid) { const workspaceStore = useWorkspaceStore() workspaceStore.setActiveSessionForWorkspace(workspaceUuid, sessionUuid) } }, async setNextActiveSession(workspaceUuid: string | null) { if (workspaceUuid && this.workspaceHistory[workspaceUuid]?.length > 0) { // Set first available session in the same workspace const nextSession = this.workspaceHistory[workspaceUuid][0] await this.setActiveSession(workspaceUuid, nextSession.uuid) } else { // Find any available session for (const [wUuid, sessions] of Object.entries(this.workspaceHistory)) { if (sessions.length > 0) { await this.setActiveSession(wUuid, sessions[0].uuid) return } } // No sessions available this.activeSessionUuid = null } }, async navigateToSession(sessionUuid: string) { // Prevent overlapping navigation calls if (this.isNavigating) { console.log('Navigation already in progress, skipping') return } this.isNavigating = true try { const session = this.getChatSessionByUuid(sessionUuid) if (session && session.workspaceUuid) { // Check if we're already on the correct route to avoid unnecessary navigation const currentRoute = router.currentRoute.value const currentWorkspaceUuid = currentRoute.params.workspaceUuid as string const currentSessionUuid = currentRoute.params.uuid as string if (currentWorkspaceUuid === session.workspaceUuid && currentSessionUuid === sessionUuid) { console.log('Already on correct route, skipping navigation') return } const workspaceStore = useWorkspaceStore() await workspaceStore.navigateToWorkspace(session.workspaceUuid, sessionUuid) } else { // If session doesn't have a workspace, try to assign it to the default workspace const workspaceStore = useWorkspaceStore() const defaultWorkspace = workspaceStore.workspaces.find(w => w.isDefault) || workspaceStore.workspaces[0] if (defaultWorkspace) { console.log('Session without workspace, navigating to default workspace:', defaultWorkspace.uuid) await workspaceStore.navigateToWorkspace(defaultWorkspace.uuid, sessionUuid) } else { // Last resort: navigate to default route console.log('No workspace available, navigating to default route') await router.push({ name: 'DefaultWorkspace' }) } } } finally { this.isNavigating = false } }, // Helper method to clear all sessions for a workspace clearWorkspaceSessions(workspaceUuid: string) { this.workspaceHistory[workspaceUuid] = [] // Clear active session if it was in this workspace const activeSession = this.activeSession if (activeSession && activeSession.workspaceUuid === workspaceUuid) { this.activeSessionUuid = null } }, // Helper method to get all sessions across all workspaces getAllSessions() { const allSessions: Chat.Session[] = [] for (const sessions of Object.values(this.workspaceHistory)) { allSessions.push(...sessions) } return allSessions }, // Legacy compatibility method - maps to createSessionInWorkspace async addSession(session: Chat.Session) { return await this.createSessionInWorkspace(session.title, session.workspaceUuid, session.model) }, // Centralized session creation method for consistent behavior async createNewSession(title?: string, workspaceUuid?: string, model?: string) { const workspaceStore = useWorkspaceStore() const targetWorkspaceUuid = workspaceUuid || workspaceStore.activeWorkspaceUuid if (!targetWorkspaceUuid) { throw new Error('No workspace available for session creation') } const sessionTitle = title || 'New Chat' return await this.createSessionInWorkspace(sessionTitle, targetWorkspaceUuid, model) }, }, }) ================================================ FILE: web/src/store/modules/user/helper.ts ================================================ import { ss } from '@/utils/storage' const LOCAL_NAME = 'userStorage' export interface UserInfo { name: string description: string } export interface UserState { userInfo: UserInfo } export function defaultSetting(): UserState { return { userInfo: { name: '', description: '', }, } } export function getLocalState(): UserState { const localSetting: UserState | undefined = ss.get(LOCAL_NAME) return { ...defaultSetting(), ...localSetting } } export function setLocalState(setting: UserState): void { ss.set(LOCAL_NAME, setting) } ================================================ FILE: web/src/store/modules/user/index.ts ================================================ import { defineStore } from 'pinia' import type { UserInfo, UserState } from './helper' import { defaultSetting, getLocalState, setLocalState } from './helper' export const useUserStore = defineStore('user-store', { state: (): UserState => getLocalState(), actions: { updateUserInfo(userInfo: Partial) { this.userInfo = { ...this.userInfo, ...userInfo } this.recordState() }, resetUserInfo() { this.userInfo = { ...defaultSetting().userInfo } this.recordState() }, recordState() { setLocalState(this.$state) }, }, }) ================================================ FILE: web/src/store/modules/workspace/index.ts ================================================ import { defineStore } from 'pinia' import { router } from '@/router' import { getWorkspaces, createWorkspace, updateWorkspace, deleteWorkspace, ensureDefaultWorkspace, setDefaultWorkspace, updateWorkspaceOrder as updateWorkspaceOrderApi, autoMigrateLegacySessions, getAllWorkspaceActiveSessions, getChatSessionDefault, getWorkspace, } from '@/api' import { useSessionStore } from '@/store/modules/session' import { t } from '@/locales' export interface WorkspaceState { workspaces: Chat.Workspace[] activeWorkspaceUuid: string | null workspaceActiveSessions: Record // workspaceUuid -> sessionUuid pendingSessionRestore: { workspaceUuid: string; sessionUuid: string } | null isLoading: boolean } export const useWorkspaceStore = defineStore('workspace-store', { state: (): WorkspaceState => ({ workspaces: [], activeWorkspaceUuid: null, workspaceActiveSessions: {}, pendingSessionRestore: null, isLoading: false, }), getters: { getWorkspaceByUuid(state) { return (uuid?: string) => { if (uuid) { return state.workspaces.find(workspace => workspace.uuid === uuid) } return null } }, getDefaultWorkspace(state) { return state.workspaces.find(workspace => workspace.isDefault) || null }, activeWorkspace(state) { if (state.activeWorkspaceUuid) { return state.workspaces.find(workspace => workspace.uuid === state.activeWorkspaceUuid) } return null }, // Get active session for a specific workspace getActiveSessionForWorkspace(state) { return (workspaceUuid: string) => { return state.workspaceActiveSessions[workspaceUuid] || null } }, }, actions: { // Optimized initialization that only loads active workspace async initializeActiveWorkspace(targetWorkspaceUuid?: string) { try { console.log('🔄 Starting optimized workspace initialization...') // Step 1: Handle legacy session migration (if needed) try { const migrationResult = await autoMigrateLegacySessions() if (migrationResult.hasLegacySessions && migrationResult.migratedSessions > 0) { console.log(`🔄 Auto-migrated ${migrationResult.migratedSessions} legacy sessions to default workspace`) } } catch (migrationError) { console.warn('⚠️ Legacy session migration failed:', migrationError) } // Step 2: Load only the active/target workspace const activeWorkspace = await this.loadActiveWorkspace(targetWorkspaceUuid) // Step 3: Sync workspace active sessions from backend (for session persistence) const routeCurrent = router.currentRoute.value const urlWorkspaceUuid = routeCurrent.params.workspaceUuid as string || targetWorkspaceUuid const urlSessionUuid = routeCurrent.params.uuid as string await this.syncWorkspaceActiveSessions(urlWorkspaceUuid, urlSessionUuid) // Step 4: Load sessions only for the active workspace const sessionStore = useSessionStore() await sessionStore.syncActiveWorkspaceSessions() // Step 5: Ensure user has at least one session in the active workspace await this.ensureUserHasSession() console.log('✅ Optimized workspace initialization completed') return activeWorkspace } catch (error) { console.error('❌ Error in optimized workspace initialization:', error) throw error } }, // Comprehensive initialization method that replaces the old chat store logic async initializeApplication() { try { console.log('🔄 Starting comprehensive application initialization...') // Step 1: Handle legacy session migration try { const migrationResult = await autoMigrateLegacySessions() if (migrationResult.hasLegacySessions && migrationResult.migratedSessions > 0) { console.log(`🔄 Auto-migrated ${migrationResult.migratedSessions} legacy sessions to default workspace`) // Only force refresh if we're not already on a workspace route const currentRoute = router.currentRoute.value if (currentRoute.name !== 'WorkspaceChat') { console.log('🔄 Refreshing page after migration') window.location.reload() return // Exit early since we're refreshing } else { console.log('🔄 Skipping refresh - already on workspace route') } } } catch (migrationError) { console.warn('⚠️ Legacy session migration failed:', migrationError) // Continue with normal sync - don't block the app } // Step 2: Sync workspaces await this.syncWorkspaces() // Step 3: Determine workspace context from URL const routeBeforeSync = router.currentRoute.value const urlWorkspaceUuid = routeBeforeSync.name === 'WorkspaceChat' ? routeBeforeSync.params.workspaceUuid as string : null const urlSessionUuid = routeBeforeSync.params.uuid as string const isOnDefaultRoute = routeBeforeSync.name === 'DefaultWorkspace' // Step 4: Sync workspace active sessions from backend await this.syncWorkspaceActiveSessions(urlWorkspaceUuid || undefined, urlSessionUuid || undefined) // Step 5: Ensure we have an active workspace await this.ensureActiveWorkspace() // Step 6: Initialize sessions through session store const sessionStore = useSessionStore() await sessionStore.syncAllWorkspaceSessions() // Step 7: Handle session creation if needed await this.ensureUserHasSession() // Step 8: Handle navigation await this.handleInitialNavigation(urlWorkspaceUuid || undefined, urlSessionUuid || undefined, isOnDefaultRoute) console.log('✅ Application initialization completed successfully') } catch (error) { console.error('❌ Error in initializeApplication:', error) throw error } }, // Optimized method to load only the active/default workspace async loadActiveWorkspace(targetWorkspaceUuid?: string) { try { this.isLoading = true // If specific workspace is requested, try to load it if (targetWorkspaceUuid) { try { const workspace = await getWorkspace(targetWorkspaceUuid) this.workspaces = [workspace] this.activeWorkspaceUuid = workspace.uuid return workspace } catch (error) { console.warn(`Failed to load specific workspace ${targetWorkspaceUuid}, falling back to default`, error) } } // Check if we already have a default workspace loaded const existingDefault = this.workspaces.find(w => w.isDefault) if (existingDefault) { this.activeWorkspaceUuid = existingDefault.uuid console.log('✅ Using existing default workspace:', existingDefault.name) return existingDefault } // Ensure we have a default workspace (this only loads/creates the default one) const defaultWorkspace = await ensureDefaultWorkspace() this.workspaces = [defaultWorkspace] this.activeWorkspaceUuid = defaultWorkspace.uuid console.log('✅ Loaded default workspace:', defaultWorkspace.name) return defaultWorkspace } catch (error) { console.error('Failed to load active workspace:', error) throw error } finally { this.isLoading = false } }, // Load additional workspaces on demand (for workspace selector) async loadAllWorkspaces() { try { const allWorkspaces = await getWorkspaces() // Replace workspaces array with all workspaces (this is for workspace selector) // Keep the active workspace UUID as is this.workspaces = allWorkspaces // Ensure activeWorkspaceUuid is still valid if (this.activeWorkspaceUuid && !allWorkspaces.find(w => w.uuid === this.activeWorkspaceUuid)) { const defaultWs = allWorkspaces.find(w => w.isDefault) || allWorkspaces[0] if (defaultWs) { this.activeWorkspaceUuid = defaultWs.uuid } } return allWorkspaces } catch (error) { console.error('Failed to load all workspaces:', error) throw error } }, async syncWorkspaceActiveSessions(urlWorkspaceUuid?: string, urlSessionUuid?: string) { try { const backendSessions = await getAllWorkspaceActiveSessions() // Build workspace active sessions mapping this.workspaceActiveSessions = {} let globalActiveSession = null for (const session of backendSessions) { if (session.workspaceUuid) { this.workspaceActiveSessions[session.workspaceUuid] = session.chatSessionUuid if (!globalActiveSession) { globalActiveSession = session } } } // Prioritize URL context over backend data if (urlWorkspaceUuid && urlSessionUuid) { this.activeWorkspaceUuid = urlWorkspaceUuid console.log('✅ Used workspace from URL:', { workspaceUuid: urlWorkspaceUuid, sessionUuid: urlSessionUuid }) // Set active session in session store const sessionStore = useSessionStore() sessionStore.setActiveSessionWithoutNavigation(urlWorkspaceUuid, urlSessionUuid) } else if (urlWorkspaceUuid) { this.activeWorkspaceUuid = urlWorkspaceUuid console.log('✅ Used workspace from URL (no session):', urlWorkspaceUuid) // Restore active session for this workspace if available const activeSessionForWorkspace = this.workspaceActiveSessions[urlWorkspaceUuid] if (activeSessionForWorkspace) { const sessionStore = useSessionStore() sessionStore.setActiveSessionWithoutNavigation(urlWorkspaceUuid, activeSessionForWorkspace) console.log('✅ Restored active session for workspace:', activeSessionForWorkspace) } } else if (globalActiveSession?.workspaceUuid) { this.activeWorkspaceUuid = globalActiveSession.workspaceUuid console.log('✅ Used workspace from backend:', globalActiveSession.workspaceUuid) // Set active session from backend const sessionStore = useSessionStore() sessionStore.setActiveSessionWithoutNavigation(globalActiveSession.workspaceUuid, globalActiveSession.chatSessionUuid) console.log('✅ Restored active session from backend:', globalActiveSession.chatSessionUuid) } } catch (error) { console.warn('⚠️ Failed to sync workspace active sessions:', error) } }, async ensureActiveWorkspace() { // If we don't have an active workspace, set to default if (!this.activeWorkspaceUuid && this.workspaces.length > 0) { const defaultWorkspace = this.workspaces.find(workspace => workspace.isDefault) || this.workspaces[0] if (defaultWorkspace) { this.activeWorkspaceUuid = defaultWorkspace.uuid console.log('✅ Set active workspace to default:', defaultWorkspace.name) } } }, async ensureUserHasSession() { const sessionStore = useSessionStore() // Check if user has any sessions const allSessions = sessionStore.getAllSessions() if (allSessions.length === 0) { console.log('🔄 No sessions found for user, creating default session') // Ensure we have a default workspace const defaultWorkspace = this.workspaces.find(workspace => workspace.isDefault) || null if (!defaultWorkspace) { console.error('❌ No default workspace found when trying to create default session') throw new Error('No default workspace available for session creation') } // Set active workspace this.activeWorkspaceUuid = defaultWorkspace.uuid // Create default session const new_chat_text = t('chat.new') await sessionStore.createSessionInWorkspace(new_chat_text, defaultWorkspace.uuid) console.log('✅ Created default session for new user') } }, async handleInitialNavigation(urlWorkspaceUuid?: string, urlSessionUuid?: string, isOnDefaultRoute?: boolean) { const sessionStore = useSessionStore() if (urlSessionUuid && sessionStore.getChatSessionByUuid(urlSessionUuid)) { // We have a valid session in URL, set it as active const session = sessionStore.getChatSessionByUuid(urlSessionUuid) if (session) { await sessionStore.setActiveSession(session.workspaceUuid || this.activeWorkspaceUuid, urlSessionUuid) console.log('✅ Set active session from URL:', urlSessionUuid) } } else if (this.activeWorkspaceUuid) { // Find a session to activate in the active workspace const workspaceSessions = sessionStore.getSessionsByWorkspace(this.activeWorkspaceUuid) if (workspaceSessions.length > 0) { await sessionStore.setActiveSession(this.activeWorkspaceUuid, workspaceSessions[0].uuid) console.log('✅ Set first session as active in workspace') } } // Handle default route navigation if (isOnDefaultRoute && this.activeWorkspaceUuid) { console.log('✅ Navigating from default route to active workspace') await router.push({ name: 'WorkspaceChat', params: { workspaceUuid: this.activeWorkspaceUuid } }) } }, async syncWorkspaces() { try { this.isLoading = true const workspaces = await getWorkspaces() this.workspaces = workspaces // Ensure we have a default workspace const defaultWorkspace = this.workspaces.find(workspace => workspace.isDefault) || null if (!defaultWorkspace) { await this.ensureDefaultWorkspace() } // Set active workspace if not already set if (!this.activeWorkspaceUuid && this.workspaces.length > 0) { const defaultWs = this.workspaces.find(workspace => workspace.isDefault) || this.workspaces[0] this.activeWorkspaceUuid = defaultWs.uuid } } catch (error) { console.error('Failed to sync workspaces:', error) throw error } finally { this.isLoading = false } }, async ensureDefaultWorkspace() { try { const defaultWorkspace = await ensureDefaultWorkspace() this.workspaces.push(defaultWorkspace) this.activeWorkspaceUuid = defaultWorkspace.uuid // Automatically create a default session for the default workspace try { const sessionStore = useSessionStore() const new_chat_text = t('chat.new') await sessionStore.createSessionInWorkspace(new_chat_text, defaultWorkspace.uuid) console.log(`✅ Created default session for default workspace: ${defaultWorkspace.name}`) } catch (sessionError) { console.warn(`⚠️ Failed to create default session for default workspace ${defaultWorkspace.name}:`, sessionError) // Don't throw here - workspace creation should succeed even if session creation fails } return defaultWorkspace } catch (error) { console.error('Failed to ensure default workspace:', error) throw error } }, async setActiveWorkspace(workspaceUuid: string) { console.log('🔄 setActiveWorkspace called with:', workspaceUuid) console.log('🔍 Current workspaces in store:', this.workspaces.map(w => ({ uuid: w.uuid, name: w.name }))) let workspace = this.workspaces.find(workspace => workspace.uuid === workspaceUuid) console.log('🔍 Found workspace in store:', workspace ? workspace.name : 'NOT FOUND') // If workspace is not loaded, load it on-demand if (!workspace) { try { console.log(`Loading workspace ${workspaceUuid} on-demand...`) workspace = await getWorkspace(workspaceUuid) this.workspaces.push(workspace) console.log(`✅ Loaded workspace on-demand: ${workspace.name}`) } catch (error) { console.error(`Failed to load workspace ${workspaceUuid}:`, error) throw new Error(`Workspace ${workspaceUuid} not found`) } } console.log('🔄 Setting activeWorkspaceUuid to:', workspaceUuid) this.activeWorkspaceUuid = workspaceUuid console.log('✅ activeWorkspaceUuid set, current value:', this.activeWorkspaceUuid) // Load sessions for this workspace if not already loaded const sessionStore = useSessionStore() const existingSessions = sessionStore.getSessionsByWorkspace(workspaceUuid) console.log('🔍 Existing sessions for workspace:', existingSessions.length) if (existingSessions.length === 0) { console.log(`Loading sessions for workspace ${workspaceUuid}...`) await sessionStore.syncWorkspaceSessions(workspaceUuid) console.log(`✅ Loaded sessions for workspace: ${workspace.name}`) } // Get the updated sessions list after potential loading const sessionsAfterLoad = sessionStore.getSessionsByWorkspace(workspaceUuid) // Restore the previously active session for this workspace if it is still valid const activeSessionForWorkspace = this.workspaceActiveSessions[workspaceUuid] const hasSessions = sessionsAfterLoad.length > 0 const isStoredSessionValid = Boolean( activeSessionForWorkspace && sessionsAfterLoad.some(session => session.uuid === activeSessionForWorkspace) ) console.log('🔍 Active session for workspace:', activeSessionForWorkspace, 'isValid:', isStoredSessionValid) if (isStoredSessionValid && activeSessionForWorkspace) { sessionStore.setActiveSessionWithoutNavigation(workspaceUuid, activeSessionForWorkspace) console.log('✅ Restored previously active session') } else if (hasSessions) { const firstSession = sessionsAfterLoad[0] console.log('🔄 Selecting first available session as active:', firstSession.title) sessionStore.setActiveSessionWithoutNavigation(workspaceUuid, firstSession.uuid) console.log('✅ Set first session as active') } else { console.log('⚠️ No sessions available in workspace; clearing active session state') this.clearActiveSessionForWorkspace(workspaceUuid) sessionStore.activeSessionUuid = null } // Emit an event that the chat view can listen to this.$patch((state) => { state.pendingSessionRestore = null // Clear any pending restore }) console.log('✅ setActiveWorkspace completed successfully') }, // Method to handle session restore (called from chat view) restoreActiveSession() { const pending = this.pendingSessionRestore if (pending) { const sessionStore = useSessionStore() const session = sessionStore.getChatSessionByUuid(pending.sessionUuid) if (session) { sessionStore.setActiveSession(pending.workspaceUuid, pending.sessionUuid) } else { // Session no longer exists, clear the tracking delete this.workspaceActiveSessions[pending.workspaceUuid] } // Clear the pending restore this.$patch((state) => { state.pendingSessionRestore = null }) } }, async createWorkspace(name: string, description: string = '', color: string = '#6366f1', icon: string = 'folder') { try { const newWorkspace = await createWorkspace({ name, description, color, icon, }) this.workspaces.push(newWorkspace) // Automatically create a default session for the new workspace try { const sessionStore = useSessionStore() const new_chat_text = t('chat.new') await sessionStore.createSessionInWorkspace(new_chat_text, newWorkspace.uuid) console.log(`✅ Created default session for new workspace: ${newWorkspace.name}`) } catch (sessionError) { console.warn(`⚠️ Failed to create default session for workspace ${newWorkspace.name}:`, sessionError) // Don't throw here - workspace creation should succeed even if session creation fails } return newWorkspace } catch (error) { console.error('Failed to create workspace:', error) throw error } }, async updateWorkspace(workspaceUuid: string, updates: any) { try { const updatedWorkspace = await updateWorkspace(workspaceUuid, updates) const index = this.workspaces.findIndex(w => w.uuid === workspaceUuid) if (index !== -1) { this.workspaces[index] = updatedWorkspace } return updatedWorkspace } catch (error) { console.error('Failed to update workspace:', error) throw error } }, async deleteWorkspace(workspaceUuid: string) { try { await deleteWorkspace(workspaceUuid) this.workspaces = this.workspaces.filter(w => w.uuid !== workspaceUuid) // Remove from active sessions tracking delete this.workspaceActiveSessions[workspaceUuid] // If we deleted the active workspace, switch to default if (this.activeWorkspaceUuid === workspaceUuid) { const defaultWorkspace = this.workspaces.find(workspace => workspace.isDefault) || null if (defaultWorkspace) { this.activeWorkspaceUuid = defaultWorkspace.uuid } else if (this.workspaces.length > 0) { this.activeWorkspaceUuid = this.workspaces[0].uuid } else { this.activeWorkspaceUuid = null } } } catch (error) { console.error('Failed to delete workspace:', error) throw error } }, async setDefaultWorkspace(workspaceUuid: string) { try { await setDefaultWorkspace(workspaceUuid) // Update local state this.workspaces.forEach(workspace => { workspace.isDefault = workspace.uuid === workspaceUuid }) } catch (error) { console.error('Failed to set default workspace:', error) throw error } }, async updateWorkspaceOrder(workspaceUuids: string[]) { try { if (!Array.isArray(workspaceUuids) || workspaceUuids.length === 0) { console.warn('updateWorkspaceOrder expects a non-empty array of UUIDs') return } // Persist order positions to backend const updatePromises = workspaceUuids.map((uuid, index) => updateWorkspaceOrderApi(uuid, index)) await Promise.all(updatePromises) // Reorder locally to reflect saved order const reorderedWorkspaces: Chat.Workspace[] = [] workspaceUuids.forEach((uuid, index) => { const workspace = this.workspaces.find(w => w.uuid === uuid) if (workspace) { reorderedWorkspaces.push({ ...workspace, orderPosition: index }) } }) // If we couldn't build a valid reordered list, avoid wiping the current state if (reorderedWorkspaces.length === 0) { console.warn('No workspaces matched the provided order list; skipping reorder') return } this.workspaces = reorderedWorkspaces } catch (error) { console.error('Failed to update workspace order:', error) throw error } }, setActiveSessionForWorkspace(workspaceUuid: string, sessionUuid: string) { this.workspaceActiveSessions[workspaceUuid] = sessionUuid }, clearActiveSessionForWorkspace(workspaceUuid: string) { delete this.workspaceActiveSessions[workspaceUuid] }, async navigateToWorkspace(workspaceUuid: string, sessionUuid?: string) { // Check if we're already on the target route to avoid unnecessary navigation const currentRoute = router.currentRoute.value const currentWorkspaceUuid = currentRoute.params.workspaceUuid as string const currentSessionUuid = currentRoute.params.uuid as string // If no sessionUuid provided, try to get the active session for this workspace let targetSessionUuid = sessionUuid if (!targetSessionUuid) { const sessionStore = useSessionStore() targetSessionUuid = sessionStore.activeSessionUuid || undefined } // More thorough route checking - only skip if route params match exactly if (currentRoute.name === 'WorkspaceChat' && currentWorkspaceUuid === workspaceUuid && currentSessionUuid === targetSessionUuid) { console.log('Already on exact target route, skipping navigation') return } // Additional check: if target matches the last requested session, also skip // This prevents navigation loops during rapid switching const sessionStore = useSessionStore() if (targetSessionUuid && sessionStore.lastRequestedSessionUuid !== targetSessionUuid && sessionStore.lastRequestedSessionUuid !== null) { console.log('Navigation target differs from last requested session, skipping to prevent loop') return } const route = targetSessionUuid ? { name: 'WorkspaceChat', params: { workspaceUuid, uuid: targetSessionUuid } } : { name: 'WorkspaceChat', params: { workspaceUuid } } console.log('Navigating to:', route) return router.push(route) }, }, }) ================================================ FILE: web/src/styles/global.less ================================================ html, body, #app { height: 100%; } body { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); } .floating-button { position: fixed; bottom: 10vh; right: 10vmin; z-index: 99; padding: 0.5em; border-radius: 50%; cursor: pointer; background-color: #4ff09a; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } ================================================ FILE: web/src/styles/lib/github-markdown.less ================================================ html.dark { .markdown-body { color-scheme: dark; --color-prettylights-syntax-comment: #8b949e; --color-prettylights-syntax-constant: #79c0ff; --color-prettylights-syntax-entity: #d2a8ff; --color-prettylights-syntax-storage-modifier-import: #c9d1d9; --color-prettylights-syntax-entity-tag: #7ee787; --color-prettylights-syntax-keyword: #ff7b72; --color-prettylights-syntax-string: #a5d6ff; --color-prettylights-syntax-variable: #ffa657; --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; --color-prettylights-syntax-invalid-illegal-bg: #8e1519; --color-prettylights-syntax-carriage-return-text: #f0f6fc; --color-prettylights-syntax-carriage-return-bg: #b62324; --color-prettylights-syntax-string-regexp: #7ee787; --color-prettylights-syntax-markup-list: #f2cc60; --color-prettylights-syntax-markup-heading: #1f6feb; --color-prettylights-syntax-markup-italic: #c9d1d9; --color-prettylights-syntax-markup-bold: #c9d1d9; --color-prettylights-syntax-markup-deleted-text: #ffdcd7; --color-prettylights-syntax-markup-deleted-bg: #67060c; --color-prettylights-syntax-markup-inserted-text: #aff5b4; --color-prettylights-syntax-markup-inserted-bg: #033a16; --color-prettylights-syntax-markup-changed-text: #ffdfb6; --color-prettylights-syntax-markup-changed-bg: #5a1e02; --color-prettylights-syntax-markup-ignored-text: #c9d1d9; --color-prettylights-syntax-markup-ignored-bg: #1158c7; --color-prettylights-syntax-meta-diff-range: #d2a8ff; --color-prettylights-syntax-brackethighlighter-angle: #8b949e; --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; --color-fg-default: #c9d1d9; --color-fg-muted: #8b949e; --color-fg-subtle: #6e7681; --color-canvas-default: #0d1117; --color-canvas-subtle: #161b22; --color-border-default: #30363d; --color-border-muted: #21262d; --color-neutral-muted: rgba(110,118,129,0.4); --color-accent-fg: #58a6ff; --color-accent-emphasis: #1f6feb; --color-attention-subtle: rgba(187,128,9,0.15); --color-danger-fg: #f85149; } } html { .markdown-body { color-scheme: light; --color-prettylights-syntax-comment: #6e7781; --color-prettylights-syntax-constant: #0550ae; --color-prettylights-syntax-entity: #8250df; --color-prettylights-syntax-storage-modifier-import: #24292f; --color-prettylights-syntax-entity-tag: #116329; --color-prettylights-syntax-keyword: #cf222e; --color-prettylights-syntax-string: #0a3069; --color-prettylights-syntax-variable: #953800; --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; --color-prettylights-syntax-invalid-illegal-bg: #82071e; --color-prettylights-syntax-carriage-return-text: #f6f8fa; --color-prettylights-syntax-carriage-return-bg: #cf222e; --color-prettylights-syntax-string-regexp: #116329; --color-prettylights-syntax-markup-list: #3b2300; --color-prettylights-syntax-markup-heading: #0550ae; --color-prettylights-syntax-markup-italic: #24292f; --color-prettylights-syntax-markup-bold: #24292f; --color-prettylights-syntax-markup-deleted-text: #82071e; --color-prettylights-syntax-markup-deleted-bg: #ffebe9; --color-prettylights-syntax-markup-inserted-text: #116329; --color-prettylights-syntax-markup-inserted-bg: #dafbe1; --color-prettylights-syntax-markup-changed-text: #953800; --color-prettylights-syntax-markup-changed-bg: #ffd8b5; --color-prettylights-syntax-markup-ignored-text: #eaeef2; --color-prettylights-syntax-markup-ignored-bg: #0550ae; --color-prettylights-syntax-meta-diff-range: #8250df; --color-prettylights-syntax-brackethighlighter-angle: #57606a; --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; --color-prettylights-syntax-constant-other-reference-link: #0a3069; --color-fg-default: #24292f; --color-fg-muted: #57606a; --color-fg-subtle: #6e7781; --color-canvas-default: #ffffff; --color-canvas-subtle: #f6f8fa; --color-border-default: #d0d7de; --color-border-muted: hsla(210,18%,87%,1); --color-neutral-muted: rgba(175,184,193,0.2); --color-accent-fg: #0969da; --color-accent-emphasis: #0969da; --color-attention-subtle: #fff8c5; --color-danger-fg: #cf222e; } } .markdown-body { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; margin: 0; color: var(--color-fg-default); background-color: var(--color-canvas-default); font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; font-size: 16px; line-height: 1.5; word-wrap: break-word; } .markdown-body .octicon { display: inline-block; fill: currentColor; vertical-align: text-bottom; } .markdown-body h1:hover .anchor .octicon-link:before, .markdown-body h2:hover .anchor .octicon-link:before, .markdown-body h3:hover .anchor .octicon-link:before, .markdown-body h4:hover .anchor .octicon-link:before, .markdown-body h5:hover .anchor .octicon-link:before, .markdown-body h6:hover .anchor .octicon-link:before { width: 16px; height: 16px; content: ' '; display: inline-block; background-color: currentColor; -webkit-mask-image: url("data:image/svg+xml,"); mask-image: url("data:image/svg+xml,"); } .markdown-body details, .markdown-body figcaption, .markdown-body figure { display: block; } .markdown-body summary { display: list-item; } .markdown-body [hidden] { display: none !important; } .markdown-body a { background-color: transparent; color: var(--color-accent-fg); text-decoration: none; } .markdown-body abbr[title] { border-bottom: none; text-decoration: underline dotted; } .markdown-body b, .markdown-body strong { font-weight: var(--base-text-weight-semibold, 600); } .markdown-body dfn { font-style: italic; } .markdown-body h1 { margin: .67em 0; font-weight: var(--base-text-weight-semibold, 600); padding-bottom: .3em; font-size: 2em; border-bottom: 1px solid var(--color-border-muted); } .markdown-body mark { background-color: var(--color-attention-subtle); color: var(--color-fg-default); } .markdown-body small { font-size: 90%; } .markdown-body sub, .markdown-body sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } .markdown-body sub { bottom: -0.25em; } .markdown-body sup { top: -0.5em; } .markdown-body img { border-style: none; max-width: 100%; box-sizing: content-box; background-color: var(--color-canvas-default); } .markdown-body code, .markdown-body kbd, .markdown-body pre, .markdown-body samp { font-family: monospace; font-size: 1em; } .markdown-body figure { margin: 1em 40px; } .markdown-body hr { box-sizing: content-box; overflow: hidden; background: transparent; border-bottom: 1px solid var(--color-border-muted); height: .25em; padding: 0; margin: 24px 0; background-color: var(--color-border-default); border: 0; } .markdown-body input { font: inherit; margin: 0; overflow: visible; font-family: inherit; font-size: inherit; line-height: inherit; } .markdown-body [type=button], .markdown-body [type=reset], .markdown-body [type=submit] { -webkit-appearance: button; } .markdown-body [type=checkbox], .markdown-body [type=radio] { box-sizing: border-box; padding: 0; } .markdown-body [type=number]::-webkit-inner-spin-button, .markdown-body [type=number]::-webkit-outer-spin-button { height: auto; } .markdown-body [type=search]::-webkit-search-cancel-button, .markdown-body [type=search]::-webkit-search-decoration { -webkit-appearance: none; } .markdown-body ::-webkit-input-placeholder { color: inherit; opacity: .54; } .markdown-body ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } .markdown-body a:hover { text-decoration: underline; } .markdown-body ::placeholder { color: var(--color-fg-subtle); opacity: 1; } .markdown-body hr::before { display: table; content: ""; } .markdown-body hr::after { display: table; clear: both; content: ""; } .markdown-body table { border-spacing: 0; border-collapse: collapse; display: block; width: max-content; max-width: 100%; overflow: auto; } .markdown-body td, .markdown-body th { padding: 0; } .markdown-body details summary { cursor: pointer; } .markdown-body details:not([open])>*:not(summary) { display: none !important; } .markdown-body a:focus, .markdown-body [role=button]:focus, .markdown-body input[type=radio]:focus, .markdown-body input[type=checkbox]:focus { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; } .markdown-body a:focus:not(:focus-visible), .markdown-body [role=button]:focus:not(:focus-visible), .markdown-body input[type=radio]:focus:not(:focus-visible), .markdown-body input[type=checkbox]:focus:not(:focus-visible) { outline: solid 1px transparent; } .markdown-body a:focus-visible, .markdown-body [role=button]:focus-visible, .markdown-body input[type=radio]:focus-visible, .markdown-body input[type=checkbox]:focus-visible { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; } .markdown-body a:not([class]):focus, .markdown-body a:not([class]):focus-visible, .markdown-body input[type=radio]:focus, .markdown-body input[type=radio]:focus-visible, .markdown-body input[type=checkbox]:focus, .markdown-body input[type=checkbox]:focus-visible { outline-offset: 0; } .markdown-body kbd { display: inline-block; padding: 3px 5px; font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; line-height: 10px; color: var(--color-fg-default); vertical-align: middle; background-color: var(--color-canvas-subtle); border: solid 1px var(--color-neutral-muted); border-bottom-color: var(--color-neutral-muted); border-radius: 6px; box-shadow: inset 0 -1px 0 var(--color-neutral-muted); } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 24px; margin-bottom: 16px; font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25; } .markdown-body h2 { font-weight: var(--base-text-weight-semibold, 600); padding-bottom: .3em; font-size: 1.5em; border-bottom: 1px solid var(--color-border-muted); } .markdown-body h3 { font-weight: var(--base-text-weight-semibold, 600); font-size: 1.25em; } .markdown-body h4 { font-weight: var(--base-text-weight-semibold, 600); font-size: 1em; } .markdown-body h5 { font-weight: var(--base-text-weight-semibold, 600); font-size: .875em; } .markdown-body h6 { font-weight: var(--base-text-weight-semibold, 600); font-size: .85em; color: var(--color-fg-muted); } .markdown-body p { margin-top: 0; margin-bottom: 10px; } .markdown-body blockquote { margin: 0; padding: 0 1em; color: var(--color-fg-muted); border-left: .25em solid var(--color-border-default); } .markdown-body ul, .markdown-body ol { margin-top: 0; margin-bottom: 0; padding-left: 2em; } .markdown-body ol ol, .markdown-body ul ol { list-style-type: lower-roman; } .markdown-body ul ul ol, .markdown-body ul ol ol, .markdown-body ol ul ol, .markdown-body ol ol ol { list-style-type: lower-alpha; } .markdown-body dd { margin-left: 0; } .markdown-body tt, .markdown-body code, .markdown-body samp { font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; font-size: 12px; } .markdown-body pre { margin-top: 0; margin-bottom: 0; font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; font-size: 12px; word-wrap: normal; } .markdown-body .octicon { display: inline-block; overflow: visible !important; vertical-align: text-bottom; fill: currentColor; } .markdown-body input::-webkit-outer-spin-button, .markdown-body input::-webkit-inner-spin-button { margin: 0; -webkit-appearance: none; appearance: none; } .markdown-body::before { display: table; content: ""; } .markdown-body::after { display: table; clear: both; content: ""; } .markdown-body>*:first-child { margin-top: 0 !important; } .markdown-body>*:last-child { margin-bottom: 0 !important; } .markdown-body a:not([href]) { color: inherit; text-decoration: none; } .markdown-body .absent { color: var(--color-danger-fg); } .markdown-body .anchor { float: left; padding-right: 4px; margin-left: -20px; line-height: 1; } .markdown-body .anchor:focus { outline: none; } .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre, .markdown-body details { margin-top: 0; margin-bottom: 16px; } .markdown-body blockquote>:first-child { margin-top: 0; } .markdown-body blockquote>:last-child { margin-bottom: 0; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: var(--color-fg-default); vertical-align: middle; visibility: hidden; } .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { text-decoration: none; } .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { visibility: visible; } .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { padding: 0 .2em; font-size: inherit; } .markdown-body summary h1, .markdown-body summary h2, .markdown-body summary h3, .markdown-body summary h4, .markdown-body summary h5, .markdown-body summary h6 { display: inline-block; } .markdown-body summary h1 .anchor, .markdown-body summary h2 .anchor, .markdown-body summary h3 .anchor, .markdown-body summary h4 .anchor, .markdown-body summary h5 .anchor, .markdown-body summary h6 .anchor { margin-left: -40px; } .markdown-body summary h1, .markdown-body summary h2 { padding-bottom: 0; border-bottom: 0; } .markdown-body ul.no-list, .markdown-body ol.no-list { padding: 0; list-style-type: none; } .markdown-body ol[type=a] { list-style-type: lower-alpha; } .markdown-body ol[type=A] { list-style-type: upper-alpha; } .markdown-body ol[type=i] { list-style-type: lower-roman; } .markdown-body ol[type=I] { list-style-type: upper-roman; } .markdown-body ol[type="1"] { list-style-type: decimal; } .markdown-body div>ol:not([type]) { list-style-type: decimal; } .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { margin-top: 0; margin-bottom: 0; } .markdown-body li>p { margin-top: 16px; } .markdown-body li+li { margin-top: .25em; } .markdown-body dl { padding: 0; } .markdown-body dl dt { padding: 0; margin-top: 16px; font-size: 1em; font-style: italic; font-weight: var(--base-text-weight-semibold, 600); } .markdown-body dl dd { padding: 0 16px; margin-bottom: 16px; } .markdown-body table th { font-weight: var(--base-text-weight-semibold, 600); } .markdown-body table th, .markdown-body table td { padding: 6px 13px; border: 1px solid var(--color-border-default); } .markdown-body table tr { background-color: var(--color-canvas-default); border-top: 1px solid var(--color-border-muted); } .markdown-body table tr:nth-child(2n) { background-color: var(--color-canvas-subtle); } .markdown-body table img { background-color: transparent; } .markdown-body img[align=right] { padding-left: 20px; } .markdown-body img[align=left] { padding-right: 20px; } .markdown-body .emoji { max-width: none; vertical-align: text-top; background-color: transparent; } .markdown-body span.frame { display: block; overflow: hidden; } .markdown-body span.frame>span { display: block; float: left; width: auto; padding: 7px; margin: 13px 0 0; overflow: hidden; border: 1px solid var(--color-border-default); } .markdown-body span.frame span img { display: block; float: left; } .markdown-body span.frame span span { display: block; padding: 5px 0 0; clear: both; color: var(--color-fg-default); } .markdown-body span.align-center { display: block; overflow: hidden; clear: both; } .markdown-body span.align-center>span { display: block; margin: 13px auto 0; overflow: hidden; text-align: center; } .markdown-body span.align-center span img { margin: 0 auto; text-align: center; } .markdown-body span.align-right { display: block; overflow: hidden; clear: both; } .markdown-body span.align-right>span { display: block; margin: 13px 0 0; overflow: hidden; text-align: right; } .markdown-body span.align-right span img { margin: 0; text-align: right; } .markdown-body span.float-left { display: block; float: left; margin-right: 13px; overflow: hidden; } .markdown-body span.float-left span { margin: 13px 0 0; } .markdown-body span.float-right { display: block; float: right; margin-left: 13px; overflow: hidden; } .markdown-body span.float-right>span { display: block; margin: 13px auto 0; overflow: hidden; text-align: right; } .markdown-body code, .markdown-body tt { padding: .2em .4em; margin: 0; font-size: 85%; white-space: break-spaces; background-color: var(--color-neutral-muted); border-radius: 6px; } .markdown-body code br, .markdown-body tt br { display: none; } .markdown-body del code { text-decoration: inherit; } .markdown-body samp { font-size: 85%; } .markdown-body pre code { font-size: 100%; } .markdown-body pre>code { padding: 0; margin: 0; word-break: normal; white-space: pre; background: transparent; border: 0; } .markdown-body .highlight { margin-bottom: 16px; } .markdown-body .highlight pre { margin-bottom: 0; word-break: normal; } .markdown-body .highlight pre, .markdown-body pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: var(--color-canvas-subtle); border-radius: 6px; } .markdown-body pre code, .markdown-body pre tt { display: inline; max-width: auto; padding: 0; margin: 0; overflow: visible; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0; } .markdown-body .csv-data td, .markdown-body .csv-data th { padding: 5px; overflow: hidden; font-size: 12px; line-height: 1; text-align: left; white-space: nowrap; } .markdown-body .csv-data .blob-num { padding: 10px 8px 9px; text-align: right; background: var(--color-canvas-default); border: 0; } .markdown-body .csv-data tr { border-top: 0; } .markdown-body .csv-data th { font-weight: var(--base-text-weight-semibold, 600); background: var(--color-canvas-subtle); border-top: 0; } .markdown-body [data-footnote-ref]::before { content: "["; } .markdown-body [data-footnote-ref]::after { content: "]"; } .markdown-body .footnotes { font-size: 12px; color: var(--color-fg-muted); border-top: 1px solid var(--color-border-default); } .markdown-body .footnotes ol { padding-left: 16px; } .markdown-body .footnotes ol ul { display: inline-block; padding-left: 16px; margin-top: 16px; } .markdown-body .footnotes li { position: relative; } .markdown-body .footnotes li:target::before { position: absolute; top: -8px; right: -8px; bottom: -8px; left: -24px; pointer-events: none; content: ""; border: 2px solid var(--color-accent-emphasis); border-radius: 6px; } .markdown-body .footnotes li:target { color: var(--color-fg-default); } .markdown-body .footnotes .data-footnote-backref g-emoji { font-family: monospace; } .markdown-body .pl-c { color: var(--color-prettylights-syntax-comment); } .markdown-body .pl-c1, .markdown-body .pl-s .pl-v { color: var(--color-prettylights-syntax-constant); } .markdown-body .pl-e, .markdown-body .pl-en { color: var(--color-prettylights-syntax-entity); } .markdown-body .pl-smi, .markdown-body .pl-s .pl-s1 { color: var(--color-prettylights-syntax-storage-modifier-import); } .markdown-body .pl-ent { color: var(--color-prettylights-syntax-entity-tag); } .markdown-body .pl-k { color: var(--color-prettylights-syntax-keyword); } .markdown-body .pl-s, .markdown-body .pl-pds, .markdown-body .pl-s .pl-pse .pl-s1, .markdown-body .pl-sr, .markdown-body .pl-sr .pl-cce, .markdown-body .pl-sr .pl-sre, .markdown-body .pl-sr .pl-sra { color: var(--color-prettylights-syntax-string); } .markdown-body .pl-v, .markdown-body .pl-smw { color: var(--color-prettylights-syntax-variable); } .markdown-body .pl-bu { color: var(--color-prettylights-syntax-brackethighlighter-unmatched); } .markdown-body .pl-ii { color: var(--color-prettylights-syntax-invalid-illegal-text); background-color: var(--color-prettylights-syntax-invalid-illegal-bg); } .markdown-body .pl-c2 { color: var(--color-prettylights-syntax-carriage-return-text); background-color: var(--color-prettylights-syntax-carriage-return-bg); } .markdown-body .pl-sr .pl-cce { font-weight: bold; color: var(--color-prettylights-syntax-string-regexp); } .markdown-body .pl-ml { color: var(--color-prettylights-syntax-markup-list); } .markdown-body .pl-mh, .markdown-body .pl-mh .pl-en, .markdown-body .pl-ms { font-weight: bold; color: var(--color-prettylights-syntax-markup-heading); } .markdown-body .pl-mi { font-style: italic; color: var(--color-prettylights-syntax-markup-italic); } .markdown-body .pl-mb { font-weight: bold; color: var(--color-prettylights-syntax-markup-bold); } .markdown-body .pl-md { color: var(--color-prettylights-syntax-markup-deleted-text); background-color: var(--color-prettylights-syntax-markup-deleted-bg); } .markdown-body .pl-mi1 { color: var(--color-prettylights-syntax-markup-inserted-text); background-color: var(--color-prettylights-syntax-markup-inserted-bg); } .markdown-body .pl-mc { color: var(--color-prettylights-syntax-markup-changed-text); background-color: var(--color-prettylights-syntax-markup-changed-bg); } .markdown-body .pl-mi2 { color: var(--color-prettylights-syntax-markup-ignored-text); background-color: var(--color-prettylights-syntax-markup-ignored-bg); } .markdown-body .pl-mdr { font-weight: bold; color: var(--color-prettylights-syntax-meta-diff-range); } .markdown-body .pl-ba { color: var(--color-prettylights-syntax-brackethighlighter-angle); } .markdown-body .pl-sg { color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); } .markdown-body .pl-corl { text-decoration: underline; color: var(--color-prettylights-syntax-constant-other-reference-link); } .markdown-body g-emoji { display: inline-block; min-width: 1ch; font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; font-size: 1em; font-style: normal !important; font-weight: var(--base-text-weight-normal, 400); line-height: 1; vertical-align: -0.075em; } .markdown-body g-emoji img { width: 1em; height: 1em; } .markdown-body .task-list-item { list-style-type: none; } .markdown-body .task-list-item label { font-weight: var(--base-text-weight-normal, 400); } .markdown-body .task-list-item.enabled label { cursor: pointer; } .markdown-body .task-list-item+.task-list-item { margin-top: 4px; } .markdown-body .task-list-item .handle { display: none; } .markdown-body .task-list-item-checkbox { margin: 0 .2em .25em -1.4em; vertical-align: middle; } .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { margin: 0 -1.6em .25em .2em; } .markdown-body .contains-task-list { position: relative; } .markdown-body .contains-task-list:hover .task-list-item-convert-container, .markdown-body .contains-task-list:focus-within .task-list-item-convert-container { display: block; width: auto; height: 24px; overflow: visible; clip: auto; } .markdown-body ::-webkit-calendar-picker-indicator { filter: invert(50%); } ================================================ FILE: web/src/styles/lib/highlight.less ================================================ html.dark { pre code.hljs { display: block; overflow-x: auto; padding: 1em } code.hljs { padding: 3px 5px } .hljs { color: #abb2bf; background: #282c34 } .hljs-keyword, .hljs-operator, .hljs-pattern-match { color: #f92672 } .hljs-function, .hljs-pattern-match .hljs-constructor { color: #61aeee } .hljs-function .hljs-params { color: #a6e22e } .hljs-function .hljs-params .hljs-typing { color: #fd971f } .hljs-module-access .hljs-module { color: #7e57c2 } .hljs-constructor { color: #e2b93d } .hljs-constructor .hljs-string { color: #9ccc65 } .hljs-comment, .hljs-quote { color: #b18eb1; font-style: italic } .hljs-doctag, .hljs-formula { color: #c678dd } .hljs-deletion, .hljs-name, .hljs-section, .hljs-selector-tag, .hljs-subst { color: #e06c75 } .hljs-literal { color: #56b6c2 } .hljs-addition, .hljs-attribute, .hljs-meta .hljs-string, .hljs-regexp, .hljs-string { color: #98c379 } .hljs-built_in, .hljs-class .hljs-title, .hljs-title.class_ { color: #e6c07b } .hljs-attr, .hljs-number, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-pseudo, .hljs-template-variable, .hljs-type, .hljs-variable { color: #d19a66 } .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-symbol, .hljs-title { color: #61aeee } .hljs-emphasis { font-style: italic } .hljs-strong { font-weight: 700 } .hljs-link { text-decoration: underline } } html { pre code.hljs { display: block; overflow-x: auto; padding: 1em } code.hljs { padding: 3px 5px; &::-webkit-scrollbar { height: 4px; } } .hljs { color: #383a42; background: #fafafa } .hljs-comment, .hljs-quote { color: #a0a1a7; font-style: italic } .hljs-doctag, .hljs-formula, .hljs-keyword { color: #a626a4 } .hljs-deletion, .hljs-name, .hljs-section, .hljs-selector-tag, .hljs-subst { color: #e45649 } .hljs-literal { color: #0184bb } .hljs-addition, .hljs-attribute, .hljs-meta .hljs-string, .hljs-regexp, .hljs-string { color: #50a14f } .hljs-attr, .hljs-number, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-pseudo, .hljs-template-variable, .hljs-type, .hljs-variable { color: #986801 } .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-symbol, .hljs-title { color: #4078f2 } .hljs-built_in, .hljs-class .hljs-title, .hljs-title.class_ { color: #c18401 } .hljs-emphasis { font-style: italic } .hljs-strong { font-weight: 700 } .hljs-link { text-decoration: underline } } ================================================ FILE: web/src/styles/lib/tailwind.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: web/src/types/chat-models.ts ================================================ export interface ChatModel { id: number name: string label: string isEnable: boolean isDefault: boolean orderNumber: number lastUsageTime: string apiType: string maxTokens?: number costPer1kTokens?: number description?: string } export interface CreateChatModelRequest { name: string label: string apiType: string isEnable?: boolean isDefault?: boolean orderNumber?: number maxTokens?: number costPer1kTokens?: number description?: string } export interface UpdateChatModelRequest { name?: string label?: string apiType?: string isEnable?: boolean isDefault?: boolean orderNumber?: number maxTokens?: number costPer1kTokens?: number description?: string } export interface ChatModelSelectOption { label: string | (() => any) value: string disabled?: boolean } export interface ChatModelsResponse { models: ChatModel[] total: number } ================================================ FILE: web/src/typings/chat.d.ts ================================================ declare namespace Chat { interface Artifact { uuid: string type: string // 'code', 'html', 'svg', 'mermaid', 'json', 'markdown' title: string content: string language?: string // for code artifacts } interface Message { uuid: string, dateTime: string text: string model?: string inversion?: boolean error?: boolean loading?: boolean isPrompt?: boolean isPin?: boolean artifacts?: Artifact[] suggestedQuestions?: string[] suggestedQuestionsLoading?: boolean suggestedQuestionsBatches?: string[][] currentSuggestedQuestionsBatch?: number suggestedQuestionsGenerating?: boolean } interface Session { uuid: string title: string isEdit: boolean maxLength?: number temperature?: number model?: string topP?: number n?: number maxTokens?: number debug?: boolean summarizeMode?: boolean exploreMode?: boolean artifactEnabled?: boolean workspaceUuid?: string } interface Workspace { uuid: string name: string description?: string color: string icon: string isDefault: boolean orderPosition?: number sessionCount?: number createdAt: string updatedAt: string } interface ActiveSession { sessionUuid: string | null workspaceUuid: string | null } interface ChatState { activeSession: ActiveSession workspaceActiveSessions: { [workspaceUuid: string]: string } // workspaceUuid -> sessionUuid workspaces: Workspace[] workspaceHistory: { [workspaceUuid: string]: Session[] } // workspaceUuid -> Session[] chat: { [uuid: string]: Message[] } } interface ConversationRequest { uuid?: string, conversationId?: string parentMessageId?: string } interface ConversationResponse { conversationId: string detail: { // rome-ignore lint/suspicious/noExplicitAny: choices: { finish_reason: string; index: number; logprobs: any; text: string }[] created: number id: string model: string object: string usage: { completion_tokens: number; prompt_tokens: number; total_tokens: number } } id: string parentMessageId: string role: string text: string } interface ChatModel { id?: number apiAuthHeader: string apiAuthKey: string apiType: string isDefault: boolean label: string name: string url: string enablePerModeRatelimit: boolean, isEnable: boolean, maxToken?: string, defaultToken?: string, orderNumber?: string, httpTimeOut?: number } interface ChatModelPrivilege { id: string chatModelName: string fullName: string userEmail: string rateLimit: string } interface Comment { uuid: string chatMessageUuid: string content: string createdAt: string authorUsername: string } } declare namespace Snapshot { interface Snapshot { uuid: string; title: string; summary: string; tags: Record; createdAt: string; typ: 'chatbot' | 'snapshot'; } interface PostLink { uuid: string; date: string; title: string; } } declare namespace Bot { interface BotAnswerHistory { id: number botUuid: string userId: number prompt: string answer: string model: string tokensUsed: number createdAt: string updatedAt: string } } ================================================ FILE: web/src/typings/global.d.ts ================================================ interface Window { $loadingBar?: import('naive-ui').LoadingBarProviderInst; $dialog?: import('naive-ui').DialogProviderInst; $message?: import('naive-ui').MessageProviderInst; $notification?: import('naive-ui').NotificationProviderInst; } interface SelectOption { label: string value: string } declare module '*.svg' { import type { DefineComponent } from 'vue' const component: DefineComponent export default component } ================================================ FILE: web/src/utils/__tests__/date.test.ts ================================================ import { expect, describe, it, beforeAll } from 'vitest' import { displayLocaleDate } from '../date' describe('displayLocaleDate', () => { beforeAll(() => { // Set a fixed timezone for all tests process.env.TZ = 'Asia/Shanghai' }) it('should format ISO date string to local date and time', () => { const isoDate = '2025-03-05T12:48:11.990824Z' const result = displayLocaleDate(isoDate) expect(result).toBe('3/5/2025 8:48 PM') }) it('should handle date without milliseconds', () => { const isoDate = '2025-02-26T08:58:48Z' const result = displayLocaleDate(isoDate) expect(result).toBe('2/26/2025 4:58 PM') }) it('should handle invalid date string', () => { const invalidDate = 'invalid-date' const result = displayLocaleDate(invalidDate) expect(result).toBe('Invalid DateTime') }) }) ================================================ FILE: web/src/utils/artifacts.ts ================================================ import { v7 as uuid } from 'uuid' // Use the Chat namespace type export type Artifact = Chat.Artifact // Generate a simple UUID for frontend use function generateUUID(): string { return uuid() } // Extract artifacts from message content (mirrors backend logic) export function extractArtifacts(content: string): Artifact[] { const artifacts: Artifact[] = [] // Pattern for HTML artifacts (check specific types first) const htmlArtifactRegex = /```html\s*\s*\n(.*?)\n```/gs const htmlMatches = content.matchAll(htmlArtifactRegex) for (const match of htmlMatches) { const title = match[1].trim() const artifactContent = match[2].trim() const artifact: Artifact = { uuid: generateUUID(), type: 'html', title, content: artifactContent, language: 'html' } artifacts.push(artifact) } // Pattern for SVG artifacts const svgArtifactRegex = /```svg\s*\s*\n(.*?)\n```/gs const svgMatches = content.matchAll(svgArtifactRegex) for (const match of svgMatches) { const title = match[1].trim() const artifactContent = match[2].trim() const artifact: Artifact = { uuid: generateUUID(), type: 'svg', title, content: artifactContent, language: 'svg' } artifacts.push(artifact) } // Pattern for Mermaid diagrams const mermaidArtifactRegex = /```mermaid\s*\s*\n(.*?)\n```/gs const mermaidMatches = content.matchAll(mermaidArtifactRegex) for (const match of mermaidMatches) { const title = match[1].trim() const artifactContent = match[2].trim() const artifact: Artifact = { uuid: generateUUID(), type: 'mermaid', title, content: artifactContent, language: 'mermaid' } artifacts.push(artifact) } // Pattern for JSON artifacts const jsonArtifactRegex = /```json\s*\s*\n(.*?)\n```/gs const jsonMatches = content.matchAll(jsonArtifactRegex) for (const match of jsonMatches) { const title = match[1].trim() const artifactContent = match[2].trim() const artifact: Artifact = { uuid: generateUUID(), type: 'json', title, content: artifactContent, language: 'json' } artifacts.push(artifact) } // Backward-compatible parsing for legacy executable markers. const executableArtifactRegex = /```(\w+)?\s*\s*\n(.*?)\n```/gs const executableMatches = content.matchAll(executableArtifactRegex) for (const match of executableMatches) { const language = match[1] || 'javascript' const title = match[2].trim() const artifactContent = match[3].trim() // Skip if already processed as HTML, SVG, Mermaid, or JSON if (language === 'html' || language === 'svg' || language === 'mermaid' || language === 'json') { continue } const artifact: Artifact = { uuid: generateUUID(), type: 'code', title, content: artifactContent, language } artifacts.push(artifact) } // Pattern for general code artifacts (exclude html, svg, mermaid, json which are handled above) const codeArtifactRegex = /```(\w+)?\s*\s*\n(.*?)\n```/gs const codeMatches = content.matchAll(codeArtifactRegex) for (const match of codeMatches) { const language = match[1] || 'text' const title = match[2].trim() const artifactContent = match[3].trim() // Skip if already processed as HTML, SVG, Mermaid, JSON, or executable if (language === 'html' || language === 'svg' || language === 'mermaid' || language === 'json') { continue } const artifact: Artifact = { uuid: generateUUID(), type: 'code', title, content: artifactContent, language } artifacts.push(artifact) } return artifacts } ================================================ FILE: web/src/utils/crypto/index.ts ================================================ import CryptoJS from 'crypto-js' const CryptoSecret = '__CRYPTO_SECRET__' // rome-ignore lint/suspicious/noExplicitAny: export function enCrypto(data: any) { const str = JSON.stringify(data) return CryptoJS.AES.encrypt(str, CryptoSecret).toString() } export function deCrypto(data: string) { const bytes = CryptoJS.AES.decrypt(data, CryptoSecret) const str = bytes.toString(CryptoJS.enc.Utf8) if (str) return JSON.parse(str) return null } ================================================ FILE: web/src/utils/date.ts ================================================ import { DateTime } from 'luxon'; export function nowISO(): string { return DateTime.now().toISO() || '' } export function getCurrentDate() { const now = DateTime.now() const formattedDate = now.toFormat('yyyy-MM-dd-HHmm-ss') return formattedDate } // 2025-03-05T12:48:11.990824Z 2025-02-26T08:58:48Z export function displayLocaleDate(ts: string) { const dateObj = DateTime.fromISO(ts) const dateString = dateObj.toFormat('D t') return dateString } export function formatYearMonth(date: Date): string { const year = date.getFullYear().toString() const month = (date.getMonth() + 1).toString().padStart(2, '0') return `${year}-${month}` } ================================================ FILE: web/src/utils/download.ts ================================================ import { getCurrentDate } from './date' export function genTempDownloadLink(imgUrl: string) { const tempLink = document.createElement('a') tempLink.style.display = 'none' tempLink.href = imgUrl // generate a file name, chat-shot-2021-08-01.png const ts = getCurrentDate() tempLink.setAttribute('download', `chat-shot-${ts}.png`) if (typeof tempLink.download === 'undefined') tempLink.setAttribute('target', '_blank') return tempLink } ================================================ FILE: web/src/utils/errorHandler.ts ================================================ import { logger } from './logger' import { useAuthStore } from '@/store' import { showNotification, showErrorNotification, showWarningNotification, showSuccessNotification, showPersistentNotification, showEnhancedErrorNotification, showEnhancedWarningNotification, showEnhancedInfoNotification } from './notificationManager' export interface ApiError { status: number message: string code?: string details?: any } export interface ErrorHandlerOptions { logError?: boolean showToast?: boolean redirectOnError?: boolean retryCount?: number retryDelay?: number } class ErrorHandler { private defaultOptions: ErrorHandlerOptions = { logError: true, showToast: true, redirectOnError: true, retryCount: 0, retryDelay: 1000, } private isNetworkError(error: any): boolean { return ( !navigator.onLine || error.message === 'Network Error' || error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' ) } private isAuthError(error: any): boolean { return error.status === 401 || error.status === 403 } private isServerError(error: any): boolean { return error.status >= 500 && error.status < 600 } private isClientError(error: any): boolean { return error.status >= 400 && error.status < 500 } private extractErrorMessage(error: any): string { if (error.response?.data?.message) { return error.response.data.message } if (error.message) { return error.message } if (typeof error === 'string') { return error } return 'An unknown error occurred' } private logApiError(method: string, url: string, error: any, options: ErrorHandlerOptions): void { if (!options.logError) return const apiError: ApiError = { status: error.response?.status || 0, message: this.extractErrorMessage(error), code: error.code, details: error.response?.data, } logger.logApiError(method, url, apiError, apiError.status) } private async handleAuthError(error: any): Promise { const authStore = useAuthStore() try { logger.debug('Attempting token refresh for auth error', 'ErrorHandler') await authStore.refreshToken() } catch (refreshError) { logger.error('Token refresh failed, clearing auth state', 'ErrorHandler', refreshError) authStore.removeToken() authStore.removeExpiresIn() // Don't redirect immediately - let the auth store handle the UI state change // The login modal will appear automatically when authStore.isValid becomes false } } private async retryRequest( requestFn: () => Promise, retryCount: number, retryDelay: number ): Promise { let lastError: any for (let attempt = 1; attempt <= retryCount; attempt++) { try { return await requestFn() } catch (error) { lastError = error if (attempt === retryCount) { throw error } if (this.isAuthError(error)) { // Don't retry auth errors throw error } logger.debug(`Retrying request (attempt ${attempt + 1}/${retryCount})`, 'ErrorHandler', { error }) // Exponential backoff const delay = retryDelay * Math.pow(2, attempt - 1) await new Promise(resolve => setTimeout(resolve, delay)) } } throw lastError } async handleApiRequest( requestFn: () => Promise, method: string, url: string, options: ErrorHandlerOptions = {} ): Promise { const finalOptions = { ...this.defaultOptions, ...options } const startTime = Date.now() try { if (finalOptions.retryCount > 0) { const result = await this.retryRequest(requestFn, finalOptions.retryCount, finalOptions.retryDelay) const duration = Date.now() - startTime logger.logApiCall(method, url, 200, duration) return result } else { const result = await requestFn() const duration = Date.now() - startTime logger.logApiCall(method, url, 200, duration) return result } } catch (error: any) { this.logApiError(method, url, error, finalOptions) // Handle network errors if (this.isNetworkError(error)) { logger.warn('Network error detected', 'ErrorHandler', { error }) throw { status: 0, message: 'Network error. Please check your internet connection.', originalError: error, } } // Handle authentication errors if (this.isAuthError(error)) { await this.handleAuthError(error) throw { status: error.status, message: 'Authentication failed. Please login again.', originalError: error, } } // Handle server errors if (this.isServerError(error)) { logger.error('Server error occurred', 'ErrorHandler', error) throw { status: error.status, message: 'Server error. Please try again later.', originalError: error, } } // Handle client errors if (this.isClientError(error)) { logger.warn('Client error occurred', 'ErrorHandler', error) throw { status: error.status, message: this.extractErrorMessage(error), originalError: error, } } // Handle unknown errors logger.error('Unknown error occurred', 'ErrorHandler', error) throw { status: 0, message: 'An unexpected error occurred.', originalError: error, } } } // Convenience method for GET requests async get(url: string, options?: ErrorHandlerOptions): Promise { return this.handleApiRequest( () => fetch(url, { method: 'GET' }), 'GET', url, options ) } // Convenience method for POST requests async post(url: string, data?: any, options?: ErrorHandlerOptions): Promise { return this.handleApiRequest( () => fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }), 'POST', url, options ) } // Convenience method for PUT requests async put(url: string, data?: any, options?: ErrorHandlerOptions): Promise { return this.handleApiRequest( () => fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }), 'PUT', url, options ) } // Convenience method for DELETE requests async delete(url: string, options?: ErrorHandlerOptions): Promise { return this.handleApiRequest( () => fetch(url, { method: 'DELETE' }), 'DELETE', url, options ) } // Global error handler for unhandled promise rejections setupGlobalErrorHandler(): void { window.addEventListener('unhandledrejection', (event) => { logger.error('Unhandled promise rejection', 'GlobalErrorHandler', event.reason) // Prevent default behavior (logging to console) event.preventDefault() // You could also show a user-friendly error message here if (event.reason instanceof Error) { console.error('An unexpected error occurred:', event.reason.message) } }) window.addEventListener('error', (event) => { logger.error('Global error occurred', 'GlobalErrorHandler', { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error, }) }) } } // Export singleton instance export const errorHandler = new ErrorHandler() // Export convenience functions export const handleApiRequest = ( requestFn: () => Promise, method: string, url: string, options?: ErrorHandlerOptions ) => errorHandler.handleApiRequest(requestFn, method, url, options) // Default export export default errorHandler ================================================ FILE: web/src/utils/format/index.ts ================================================ /** * 转义 HTML 字符 * @param source */ export function encodeHTML(source: string) { return source .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } /** * 判断是否为代码块 * @param text */ export function includeCode(text: string | null | undefined) { const regexp = /^(?:\s{4}|\t).+/gm return !!(text?.includes(' = ') || text?.match(regexp)) } /** * 复制文本 * @param options */ export function copyText(options: { text: string; origin?: boolean }) { const props = { origin: true, ...options } let input: HTMLInputElement | HTMLTextAreaElement if (props.origin) input = document.createElement('textarea') else input = document.createElement('input') input.setAttribute('readonly', 'readonly') input.value = props.text document.body.appendChild(input) input.select() if (document.execCommand('copy')) document.execCommand('copy') document.body.removeChild(input) } ================================================ FILE: web/src/utils/is/index.ts ================================================ export function isNumber(value: T | unknown): value is number { return Object.prototype.toString.call(value) === '[object Number]' } export function isString(value: T | unknown): value is string { return Object.prototype.toString.call(value) === '[object String]' } export function isBoolean(value: T | unknown): value is boolean { return Object.prototype.toString.call(value) === '[object Boolean]' } export function isNull(value: T | unknown): value is null { return Object.prototype.toString.call(value) === '[object Null]' } export function isUndefined(value: T | unknown): value is undefined { return Object.prototype.toString.call(value) === '[object Undefined]' } export function isObject(value: T | unknown): value is object { return Object.prototype.toString.call(value) === '[object Object]' } export function isArray(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Array]' } export function isFunction any | void | never>(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Function]' } export function isDate(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Date]' } export function isRegExp(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object RegExp]' } export function isPromise>(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Promise]' } export function isSet>(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Set]' } export function isMap>(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object Map]' } export function isFile(value: T | unknown): value is T { return Object.prototype.toString.call(value) === '[object File]' } export const isASCII = (str: string) => /^[\x00-\x7F]*$/.test(str) ================================================ FILE: web/src/utils/jwt.ts ================================================ import jwt_decode from 'jwt-decode' export function isAdmin(token: string): boolean { if (token) { const decoded: { role: string } = jwt_decode(token) if (decoded && decoded.role === 'admin') return true } return false } ================================================ FILE: web/src/utils/logger.ts ================================================ export enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, } export interface LogEntry { level: LogLevel message: string timestamp: string context?: string data?: any } class Logger { private level: LogLevel private isProduction: boolean private logs: LogEntry[] = [] constructor() { this.level = this.getLogLevelFromEnv() this.isProduction = (import.meta as any).env?.VITE_ENV === 'production' } private getLogLevelFromEnv(): LogLevel { const envLevel = (import.meta as any).env?.VITE_LOG_LEVEL || 'info' switch (envLevel) { case 'debug': return LogLevel.DEBUG case 'info': return LogLevel.INFO case 'warn': return LogLevel.WARN case 'error': return LogLevel.ERROR default: return this.isProduction ? LogLevel.WARN : LogLevel.DEBUG } } private shouldLog(level: LogLevel): boolean { return level >= this.level } private createLogEntry(level: LogLevel, message: string, context?: string, data?: any): LogEntry { return { level, message, timestamp: new Date().toISOString(), context, data, } } private formatMessage(entry: LogEntry): string { const levelName = LogLevel[entry.level] const contextStr = entry.context ? `[${entry.context}] ` : '' const dataStr = entry.data ? ` ${JSON.stringify(entry.data)}` : '' return `${entry.timestamp} [${levelName}] ${contextStr}${entry.message}${dataStr}` } private log(level: LogLevel, message: string, context?: string, data?: any): void { if (!this.shouldLog(level)) { return } const entry = this.createLogEntry(level, message, context, data) this.logs.push(entry) // Only log to console in development or for errors/warnings if (!this.isProduction || level >= LogLevel.ERROR) { const formattedMessage = this.formatMessage(entry) switch (level) { case LogLevel.DEBUG: console.debug(formattedMessage) break case LogLevel.INFO: console.info(formattedMessage) break case LogLevel.WARN: console.warn(formattedMessage) break case LogLevel.ERROR: console.error(formattedMessage) break } } } // Public logging methods debug(message: string, context?: string, data?: any): void { this.log(LogLevel.DEBUG, message, context, data) } info(message: string, context?: string, data?: any): void { this.log(LogLevel.INFO, message, context, data) } warn(message: string, context?: string, data?: any): void { this.log(LogLevel.WARN, message, context, data) } error(message: string, context?: string, data?: any): void { this.log(LogLevel.ERROR, message, context, data) } // Specialized logging methods for common scenarios logApiCall(method: string, url: string, status?: number, duration?: number): void { this.debug(`API ${method} ${url}`, 'API', { method, url, status, duration }) } logApiError(method: string, url: string, error: any, status?: number): void { this.error(`API Error ${method} ${url}`, 'API', { method, url, error, status }) } logStoreAction(action: string, store: string, data?: any): void { this.debug(`Store action: ${action}`, store, data) } logPerformance(metric: string, value: number, unit: string = 'ms'): void { this.debug(`Performance: ${metric} = ${value}${unit}`, 'Performance', { metric, value, unit }) } logUserAction(action: string, details?: any): void { this.info(`User action: ${action}`, 'User', details) } // Get logs for debugging getLogs(level?: LogLevel): LogEntry[] { if (level !== undefined) { return this.logs.filter(log => log.level >= level) } return [...this.logs] } // Clear logs clearLogs(): void { this.logs = [] } // Set log level dynamically setLevel(level: LogLevel): void { this.level = level } // Export logs for debugging exportLogs(): string { return JSON.stringify(this.logs, null, 2) } } // Export singleton instance export const logger = new Logger() // Export convenience functions for direct use export const debug = (message: string, context?: string, data?: any) => logger.debug(message, context, data) export const info = (message: string, context?: string, data?: any) => logger.info(message, context, data) export const warn = (message: string, context?: string, data?: any) => logger.warn(message, context, data) export const error = (message: string, context?: string, data?: any) => logger.error(message, context, data) // Default export export default logger ================================================ FILE: web/src/utils/notificationManager.ts ================================================ import { ref, computed, h } from 'vue' import { useMessage } from 'naive-ui' import EnhancedNotification from '@/components/common/EnhancedNotification.vue' interface NotificationOptions { title?: string message: string type?: 'success' | 'error' | 'warning' | 'info' duration?: number action?: { text: string onClick: () => void } persistent?: boolean closable?: boolean enhanced?: boolean // New option to use enhanced notifications } interface QueuedNotification { id: string options: NotificationOptions timestamp: Date } class NotificationManager { private queue = ref([]) private activeNotifications = ref>(new Set()) private messageInstance: any = null private maxConcurrent = 3 private queueEnabled = true setMessageInstance(instance: any) { this.messageInstance = instance } private generateId(): string { return `notification_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` } private canShowNotification(): boolean { return this.activeNotifications.value.size < this.maxConcurrent } private showNotification(notification: QueuedNotification) { if (!this.messageInstance) return const { id, options } = notification this.activeNotifications.value.add(id) const showFn = this.messageInstance[options.type || 'info'] const notificationOptions: any = { duration: options.persistent ? 0 : (options.duration || 3000), closable: options.closable !== false, keepAliveOnHover: true, onLeave: () => { this.activeNotifications.value.delete(id) this.processQueue() } } if (options.action) { notificationOptions.action = options.action } try { // Use enhanced notification if requested if (options.enhanced) { const content = h(EnhancedNotification, { type: options.type || 'info', title: options.title, content: options.message, closable: options.closable !== false, action: options.action, onClose: () => { this.activeNotifications.value.delete(id) this.processQueue() } }) showFn(content, { ...notificationOptions, closable: false // Let the component handle closing }) } else { showFn(options.message, notificationOptions) } } catch (error) { console.error('Failed to show notification:', error) this.activeNotifications.value.delete(id) this.processQueue() } } private processQueue() { if (!this.queueEnabled || !this.canShowNotification()) return const nextNotification = this.queue.value.shift() if (nextNotification) { this.showNotification(nextNotification) } } show(options: NotificationOptions): string { const id = this.generateId() const notification: QueuedNotification = { id, options, timestamp: new Date() } if (this.canShowNotification()) { this.showNotification(notification) } else { this.queue.value.push(notification) } return id } success(message: string, options: Omit = {}): string { return this.show({ message, type: 'success', ...options }) } error(message: string, options: Omit = {}): string { return this.show({ message, type: 'error', ...options }) } warning(message: string, options: Omit = {}): string { return this.show({ message, type: 'warning', ...options }) } info(message: string, options: Omit = {}): string { return this.show({ message, type: 'info', ...options }) } // Enhanced notification methods with better visual hierarchy enhancedSuccess(title: string, message: string, options: Omit = {}): string { return this.show({ title, message, type: 'success', enhanced: true, ...options }) } enhancedError(title: string, message: string, options: Omit = {}): string { return this.show({ title, message, type: 'error', enhanced: true, ...options }) } enhancedWarning(title: string, message: string, options: Omit = {}): string { return this.show({ title, message, type: 'warning', enhanced: true, ...options }) } enhancedInfo(title: string, message: string, options: Omit = {}): string { return this.show({ title, message, type: 'info', enhanced: true, ...options }) } persistent(message: string, type: 'error' | 'warning' | 'info' = 'error', action?: { text: string; onClick: () => void }): string { return this.show({ message, type, persistent: true, action }) } remove(id: string): void { this.queue.value = this.queue.value.filter(n => n.id !== id) this.activeNotifications.value.delete(id) } clear(): void { this.queue.value = [] this.activeNotifications.value.clear() if (this.messageInstance) { try { this.messageInstance.destroyAll() } catch (error) { console.warn('Failed to clear notifications:', error) } } } getStats() { return { queued: this.queue.value.length, active: this.activeNotifications.value.size, maxConcurrent: this.maxConcurrent } } setMaxConcurrent(max: number): void { this.maxConcurrent = max this.processQueue() } enableQueue(): void { this.queueEnabled = true this.processQueue() } disableQueue(): void { this.queueEnabled = false } } // Export singleton instance export const notificationManager = new NotificationManager() // Vue composable for easy usage in components export function useNotification() { const message = useMessage() // Initialize message instance if not already set if (!notificationManager['messageInstance']) { notificationManager.setMessageInstance(message) } return { show: notificationManager.show.bind(notificationManager), success: notificationManager.success.bind(notificationManager), error: notificationManager.error.bind(notificationManager), warning: notificationManager.warning.bind(notificationManager), info: notificationManager.info.bind(notificationManager), enhancedSuccess: notificationManager.enhancedSuccess.bind(notificationManager), enhancedError: notificationManager.enhancedError.bind(notificationManager), enhancedWarning: notificationManager.enhancedWarning.bind(notificationManager), enhancedInfo: notificationManager.enhancedInfo.bind(notificationManager), persistent: notificationManager.persistent.bind(notificationManager), clear: notificationManager.clear.bind(notificationManager), stats: computed(() => notificationManager.getStats()) } } // Global notification functions for non-Vue contexts export function showNotification(options: NotificationOptions): string { return notificationManager.show(options) } export function showSuccessNotification(message: string, options?: Omit): string { return notificationManager.success(message, options) } export function showErrorNotification(message: string, options?: Omit): string { return notificationManager.error(message, options) } export function showWarningNotification(message: string, options?: Omit): string { return notificationManager.warning(message, options) } export function showInfoNotification(message: string, options?: Omit): string { return notificationManager.info(message, options) } export function showPersistentNotification(message: string, type: 'error' | 'warning' | 'info' = 'error', action?: { text: string; onClick: () => void }): string { return notificationManager.persistent(message, type, action) } // Enhanced notification functions with better visual hierarchy export function showEnhancedSuccessNotification(title: string, message: string, options?: Omit): string { return notificationManager.enhancedSuccess(title, message, options) } export function showEnhancedErrorNotification(title: string, message: string, options?: Omit): string { return notificationManager.enhancedError(title, message, options) } export function showEnhancedWarningNotification(title: string, message: string, options?: Omit): string { return notificationManager.enhancedWarning(title, message, options) } export function showEnhancedInfoNotification(title: string, message: string, options?: Omit): string { return notificationManager.enhancedInfo(title, message, options) } export function clearAllNotifications(): void { notificationManager.clear() } ================================================ FILE: web/src/utils/prompt.ts ================================================ ================================================ FILE: web/src/utils/rand.ts ================================================ /** * Generates a random string of length n * @param n Length of the random string */ export function generateRandomString(n: number): string { // Array of possible characters const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' // Random string to be returned let randomString = '' // Loop n times to generate a string of length n for (let i = 0; i < n; i++) { // Get a random character from the array of characters const randomCharacter = characters.charAt(Math.floor(Math.random() * characters.length)) // Append the random character to the random string randomString += randomCharacter } return randomString } ================================================ FILE: web/src/utils/request/axios.ts ================================================ import axios, { type AxiosResponse } from 'axios' import { useAuthStore } from '@/store' import { logger } from '@/utils/logger' const service = axios.create({ baseURL: "/api", withCredentials: true, // Include httpOnly cookies (for refresh token) }) service.interceptors.request.use( async (config) => { const authStore = useAuthStore() // Skip token validation for authentication endpoints const authEndpoints = ['/login', '/signup', '/logout', '/auth/refresh'] if (authEndpoints.some(endpoint => config.url?.includes(endpoint))) { return config } // Wait for auth initialization to complete before making API calls if (authStore.isInitializing) { logger.debug('Waiting for auth initialization to complete', 'Axios', { url: config.url }) await authStore.waitForInitialization() if (authStore.isInitializing) { logger.warn('Auth initialization still in progress after timeout', 'Axios', { url: config.url }) } else { logger.debug('Auth initialization completed', 'Axios', { url: config.url }) } } // Check if token is expired before making request if (!authStore.isValid) { logger.debug('Token is expired or invalid, attempting refresh', 'Axios', { url: config.url }) try { await authStore.refreshToken() // Check again after refresh if (!authStore.isValid) { logger.warn('Token still invalid after refresh attempt', 'Axios') return Promise.reject(new Error('Authentication required')) } } catch (error) { logger.error('Token refresh failed in request interceptor', 'Axios', error) return Promise.reject(new Error('Authentication required')) } } // Check if token needs refresh (expires within 5 minutes) else if (authStore.needsRefresh && !authStore.isRefreshing) { logger.debug('Token needs refresh, refreshing proactively', 'Axios', { url: config.url }) try { await authStore.refreshToken() } catch (error) { logger.error('Proactive token refresh failed', 'Axios', error) // Continue with existing token if proactive refresh fails } } // Add access token to Authorization header const token = authStore.getToken if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => { return Promise.reject(error.response) }, ) service.interceptors.response.use( (response: AxiosResponse): AxiosResponse => { if (response.status === 200 || response.status === 201 || response.status === 204) return response throw new Error(response.status.toString()) }, async (error) => { const authStore = useAuthStore() logger.logApiError(error.config?.method || 'unknown', error.config?.url || 'unknown', error, error.response?.status) // Handle 401 errors with automatic token refresh if (error.response?.status === 401 && !error.config?.url?.includes('/auth/')) { logger.debug('Handling 401 error, attempting token refresh', 'Axios') // Prevent infinite retry loops if (error.config._retryCount >= 1) { logger.warn('Already retried once, clearing auth state', 'Axios') authStore.removeToken() authStore.removeExpiresIn() return Promise.reject(new Error('Authentication failed after retry')) } try { await authStore.refreshToken() // Check if refresh was successful if (!authStore.isValid) { logger.warn('Token invalid after refresh attempt', 'Axios') authStore.removeToken() authStore.removeExpiresIn() return Promise.reject(new Error('Authentication failed')) } // Retry the original request with new token const token = authStore.getToken if (token) { logger.debug('Retrying request with new token', 'Axios') error.config.headers.Authorization = `Bearer ${token}` error.config._retryCount = (error.config._retryCount || 0) + 1 return service.request(error.config) } } catch (refreshError) { // Refresh failed - user needs to login again logger.warn('Token refresh failed, clearing auth state', 'Axios', refreshError) authStore.removeToken() authStore.removeExpiresIn() return Promise.reject(new Error('Authentication required')) } } return Promise.reject(error) }, ) export default service ================================================ FILE: web/src/utils/request/index.ts ================================================ import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios' import request from './axios' import { useAuthStore } from '@/store' export interface HttpOption { url: string // rome-ignore lint/suspicious/noExplicitAny: data?: any method?: string // rome-ignore lint/suspicious/noExplicitAny: headers?: any onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void signal?: GenericAbortSignal beforeRequest?: () => void afterRequest?: () => void } export interface Response { data: T message: string | null status: string } function http( { url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, ) { const successHandler = (res: AxiosResponse>) => { const authStore = useAuthStore() if (res.data.status === 'Success' || typeof res.data === 'string') return res.data if (res.data.status === 'Unauthorized') { authStore.removeToken() window.location.reload() } return Promise.reject(res.data) } const failHandler = (error: any) => { afterRequest?.() // Enhanced error handling with more detailed error information let errorMessage = 'An unexpected error occurred' let errorCode = 'UNKNOWN_ERROR' if (error?.response?.data) { errorMessage = error.response.data.message || errorMessage errorCode = error.response.data.code || errorCode } else if (error?.message) { errorMessage = error.message } else if (typeof error === 'string') { errorMessage = error } // Create enhanced error object with proper typing interface EnhancedError extends Error { code?: string | number status?: number originalError?: any } const enhancedError = new Error(errorMessage) as EnhancedError enhancedError.name = errorCode enhancedError.code = errorCode enhancedError.status = error?.response?.status || 0 enhancedError.originalError = error throw enhancedError } beforeRequest?.() method = method || 'GET' const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {}) return method === 'GET' ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler) : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler) } export function get( { url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, ): Promise> { return http({ url, method, data, onDownloadProgress, signal, beforeRequest, afterRequest, }) } export function post( { url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, ): Promise> { return http({ url, method, data, headers, onDownloadProgress, signal, beforeRequest, afterRequest, }) } export default post ================================================ FILE: web/src/utils/sanitize.ts ================================================ const BLOCKED_TAGS = new Set(['script', 'iframe', 'object', 'embed']) const BLOCKED_SVG_TAGS = new Set(['script', 'foreignobject']) const stripUnsafeAttributes = (element: Element) => { for (const attr of Array.from(element.attributes)) { const name = attr.name.toLowerCase() const value = attr.value.trim().toLowerCase() if (name.startsWith('on')) { element.removeAttribute(attr.name) continue } if ((name === 'href' || name === 'src' || name === 'xlink:href') && value.startsWith('javascript:')) { element.removeAttribute(attr.name) } } } const sanitizeElementTree = (root: Element, blockedTags: Set) => { const ownerDocument = root.ownerDocument || document const walker = ownerDocument.createTreeWalker(root, NodeFilter.SHOW_ELEMENT) const toRemove: Element[] = [] let current = walker.currentNode as Element while (current) { const tagName = current.tagName.toLowerCase() if (blockedTags.has(tagName)) { toRemove.push(current) } else { stripUnsafeAttributes(current) } current = walker.nextNode() as Element } toRemove.forEach(node => node.remove()) } export const sanitizeHtml = (input: string): string => { try { const parser = new DOMParser() const doc = parser.parseFromString(input, 'text/html') sanitizeElementTree(doc.body, BLOCKED_TAGS) return doc.body.innerHTML } catch { return '' } } export const sanitizeSvg = (input: string): string => { try { const parser = new DOMParser() const doc = parser.parseFromString(input, 'image/svg+xml') const root = doc.documentElement if (!root) return '' sanitizeElementTree(root, BLOCKED_SVG_TAGS) return new XMLSerializer().serializeToString(root) } catch { return '' } } ================================================ FILE: web/src/utils/storage/index.ts ================================================ export * from './local' ================================================ FILE: web/src/utils/storage/local.ts ================================================ import { deCrypto, enCrypto } from '../crypto' interface StorageData { data: T expire: number | null } export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) { const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 const { expire, crypto } = Object.assign( { expire: DEFAULT_CACHE_TIME, crypto: true, }, options, ) function set(key: string, data: T) { const storageData: StorageData = { data, expire: expire !== null ? new Date().getTime() + expire * 1000 : null, } const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData) window.localStorage.setItem(key, json) } function get(key: string) { const json = window.localStorage.getItem(key) if (json) { // rome-ignore lint/suspicious/noExplicitAny: let storageData: StorageData | null = null try { storageData = crypto ? deCrypto(json) : JSON.parse(json) } catch { // Prevent failure } if (storageData) { const { data, expire } = storageData if (expire === null || expire >= Date.now()) return data } remove(key) return null } } function remove(key: string) { window.localStorage.removeItem(key) } function clear() { window.localStorage.clear() } return { set, get, remove, clear, } } export const ls = createLocalStorage() export const ss = createLocalStorage({ expire: null, crypto: false }) ================================================ FILE: web/src/utils/string.ts ================================================ export function extractStreamingData(streamResponse: string): string { const DATA_MARKER = 'data:' const SSE_DATA_MARKER = '\n\ndata:' // Handle single data segment at response start (most common after buffer split) if (streamResponse.startsWith(DATA_MARKER)) { return streamResponse.slice(DATA_MARKER.length).trim() } // Handle Server-Sent Events with multiple data segments - extract the last one const lastSSEDataPosition = streamResponse.lastIndexOf(SSE_DATA_MARKER) if (lastSSEDataPosition === -1) { return streamResponse.trim() // No SSE format detected, return original } // Extract data after the last SSE marker const dataStartPosition = lastSSEDataPosition + SSE_DATA_MARKER.length return streamResponse.slice(dataStartPosition).trim() } export function escapeDollarNumber(text: string) { let escapedText = '' for (let i = 0; i < text.length; i += 1) { let char = text[i] const nextChar = text[i + 1] || ' ' if (char === '$' && nextChar >= '0' && nextChar <= '9') char = '\\$' escapedText += char } return escapedText } export function escapeBrackets(text: string) { const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g return text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => { if (codeBlock) return codeBlock else if (squareBracket) return `$$${squareBracket}$$` else if (roundBracket) return `$${roundBracket}$` return match }) } ================================================ FILE: web/src/utils/tooling.ts ================================================ export type ToolCall = { name: string arguments: Record } const toolCallRegex = /```tool_call\s*([\s\S]*?)```/gi const toolResultRegex = /```tool_result\s*([\s\S]*?)```/gi export const extractToolCalls = (text: string) => { const calls: ToolCall[] = [] let cleanedText = text cleanedText = cleanedText.replace(toolCallRegex, (_, jsonPayload) => { try { const parsed = JSON.parse(jsonPayload.trim()) if (parsed && typeof parsed === 'object' && parsed.name) { calls.push(parsed as ToolCall) } } catch { // Ignore malformed tool calls. } return '' }) return { calls, cleanedText: cleanedText.trim(), } } export const stripToolBlocks = (text: string) => { return text.replace(toolCallRegex, '').replace(toolResultRegex, '').trim() } export const isToolResultMessage = (text: string) => { const trimmed = text.trim() return trimmed.startsWith('[[TOOL_RESULT]]') || toolResultRegex.test(trimmed) } ================================================ FILE: web/src/utils/workspaceUrls.ts ================================================ /** * Utility functions for generating and handling workspace-aware URLs */ // Get base URL for the application function getBaseUrl(): string { return `${window.location.protocol}//${window.location.host}` } // Generate shareable URL for a session within a workspace export function generateSessionUrl(sessionUuid: string, workspaceUuid?: string): string { const baseUrl = getBaseUrl() if (workspaceUuid) { return `${baseUrl}/#/workspace/${workspaceUuid}/chat/${sessionUuid}` } return `${baseUrl}/#/chat/${sessionUuid}` } // Generate shareable URL for a workspace export function generateWorkspaceUrl(workspaceUuid: string): string { const baseUrl = getBaseUrl() return `${baseUrl}/#/workspace/${workspaceUuid}/chat` } // Extract workspace and session UUIDs from a URL export function parseWorkspaceUrl(url: string): { workspaceUuid?: string; sessionUuid?: string } { try { const urlObj = new URL(url) const hash = urlObj.hash.substring(1) // Remove the # character // Match patterns: /workspace/:workspaceUuid/chat/:sessionUuid? or /chat/:sessionUuid? const workspaceMatch = hash.match(/^\/workspace\/([^\/]+)\/chat\/?([^\/]+)?/) const chatMatch = hash.match(/^\/chat\/?([^\/]+)?/) if (workspaceMatch) { return { workspaceUuid: workspaceMatch[1], sessionUuid: workspaceMatch[2] } } if (chatMatch) { return { sessionUuid: chatMatch[1] } } return {} } catch (error) { console.error('Error parsing workspace URL:', error) return {} } } // Check if a URL is a valid workspace URL export function isValidWorkspaceUrl(url: string): boolean { const parsed = parseWorkspaceUrl(url) return parsed.workspaceUuid !== undefined || parsed.sessionUuid !== undefined } // Copy URL to clipboard with error handling export async function copyUrlToClipboard(url: string): Promise { try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(url) return true } else { // Fallback for older browsers or non-HTTPS const textArea = document.createElement('textarea') textArea.value = url textArea.style.position = 'fixed' textArea.style.left = '-999999px' textArea.style.top = '-999999px' document.body.appendChild(textArea) textArea.focus() textArea.select() const success = document.execCommand('copy') document.body.removeChild(textArea) return success } } catch (error) { console.error('Failed to copy URL to clipboard:', error) return false } } // Generate QR code data URL for sharing (requires qr-code library) export function generateQRCodeUrl(url: string): string { // This would require a QR code library like 'qrcode' // For now, return a placeholder return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(url)}` } // Validate workspace UUID format export function isValidWorkspaceUuid(uuid: string): boolean { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i return uuidRegex.test(uuid) } // Validate session UUID format export function isValidSessionUuid(uuid: string): boolean { return isValidWorkspaceUuid(uuid) // Same format } // Create a URL-safe workspace name for potential future slug-based URLs export function createWorkspaceSlug(name: string): string { return name .toLowerCase() .replace(/[^\w\s-]/g, '') // Remove special characters .replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphens .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens } // Social sharing URLs export const socialShareUrls = { twitter: (url: string, text: string = 'Check out this chat workspace') => `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`, facebook: (url: string) => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, linkedin: (url: string, title: string = 'Chat Workspace') => `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`, email: (url: string, subject: string = 'Chat Workspace', body: string = 'Check out this workspace:') => `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}%20${encodeURIComponent(url)}` } ================================================ FILE: web/src/views/admin/index.vue ================================================ ================================================ FILE: web/src/views/admin/model/AddModelForm.vue ================================================ ================================================ FILE: web/src/views/admin/model/index.vue ================================================ ================================================ FILE: web/src/views/admin/modelRateLimit/addChatModelForm.vue ================================================ ================================================ FILE: web/src/views/admin/modelRateLimit/index.vue ================================================ ================================================ FILE: web/src/views/admin/user/index.vue ================================================ ================================================ FILE: web/src/views/bot/all.vue ================================================ ================================================ FILE: web/src/views/bot/components/AnswerHistory.vue ================================================ ================================================ FILE: web/src/views/bot/components/Message/index.vue ================================================ ================================================ FILE: web/src/views/bot/components/Message/style.less ================================================ .markdown-body { background-color: transparent; font-size: 14px; p { white-space: pre-wrap; } ol { list-style-type: decimal; } ul { list-style-type: disc; } pre code, pre tt { line-height: 1.65; } .highlight pre, pre { background-color: #fff; } code.hljs { padding: 0; } .code-block { &-wrapper { position: relative; padding-top: 24px; } &-header { position: absolute; top: 5px; right: 0; width: 100%; padding: 0 1rem; display: flex; justify-content: flex-end; align-items: center; color: #b3b3b3; &__copy{ cursor: pointer; margin-left: 0.5rem; user-select: none; &:hover { color: #65a665; } } } } } html.dark { .highlight pre, pre { background-color: #282c34; } } ================================================ FILE: web/src/views/bot/page.vue ================================================ ================================================ FILE: web/src/views/chat/components/ArtifactGallery.vue ================================================ ================================================ FILE: web/src/views/chat/components/AudioPlayer/index.vue ================================================ ================================================ FILE: web/src/views/chat/components/Conversation.vue ================================================ ================================================ FILE: web/src/views/chat/components/HeaderMobile/index.vue ================================================ ================================================ FILE: web/src/views/chat/components/JumpToBottom.vue ================================================ ================================================ FILE: web/src/views/chat/components/Message/ArtifactContent.vue ================================================ ================================================ FILE: web/src/views/chat/components/Message/ArtifactHeader.vue ================================================ ================================================ FILE: web/src/views/chat/components/Message/ArtifactViewer.vue ================================================ ================================================ FILE: web/src/views/chat/components/Message/ArtifactViewerBase.vue ================================================ ================================================ FILE: web/src/views/chat/components/Message/SuggestedQuestions.vue ================================================ ================================================ FILE: web/src/views/chat/components/Message/index.vue ================================================ ================================================ FILE: web/src/views/chat/components/MessageList.vue ================================================ ================================================ FILE: web/src/views/chat/components/ModelSelector.vue ================================================ ================================================ FILE: web/src/views/chat/components/PromptGallery/PromptCards.vue ================================================ ================================================ FILE: web/src/views/chat/components/PromptGallery/index.vue ================================================ ================================================ FILE: web/src/views/chat/components/RenderMessage.vue ================================================ ================================================ FILE: web/src/views/chat/components/Session/SessionConfig.vue ================================================ ================================================ FILE: web/src/views/chat/components/UploadModal.vue ================================================ ================================================ FILE: web/src/views/chat/components/Uploader.vue ================================================ ================================================ FILE: web/src/views/chat/components/UploaderReadOnly.vue ================================================ ================================================ FILE: web/src/views/chat/components/WorkspaceSelector/WorkspaceCard.vue ================================================ ================================================ FILE: web/src/views/chat/components/WorkspaceSelector/WorkspaceManagementModal.vue ================================================ ================================================ FILE: web/src/views/chat/components/WorkspaceSelector/WorkspaceModal.vue ================================================ ================================================ FILE: web/src/views/chat/components/WorkspaceSelector/index.vue ================================================ ================================================ FILE: web/src/views/chat/components/__tests__/modelSelectorUtils.test.ts ================================================ import { describe, expect, it } from 'vitest' import { getInitialModelState } from '../modelSelectorUtils' describe('getInitialModelState', () => { it('uses the default model without committing when session model is missing', () => { const result = getInitialModelState(undefined, 'model-a') expect(result).toEqual({ initialModel: 'model-a', shouldCommit: false }) }) it('commits when the session already has a model', () => { const result = getInitialModelState('model-a', 'model-b') expect(result).toEqual({ initialModel: 'model-a', shouldCommit: true }) }) }) ================================================ FILE: web/src/views/chat/components/modelSelectorUtils.ts ================================================ export const getInitialModelState = (sessionModel?: string, defaultModel?: string) => { return { initialModel: sessionModel ?? defaultModel, shouldCommit: Boolean(sessionModel), } } ================================================ FILE: web/src/views/chat/composables/README.md ================================================ # Chat Composables This directory contains refactored composables that break down the large Conversation.vue component into smaller, more manageable and reusable pieces. ## Composables Overview ### Core Functionality #### `useStreamHandling.ts` Handles all streaming-related functionality for chat responses. **Features:** - Stream progress handling with proper error management - Type-safe stream chunk processing - Centralized error formatting with i18n support - Robust error handling for various response statuses **Key Functions:** - `handleStreamProgress()` - Processes incoming stream data - `processStreamChunk()` - Parses and validates stream chunks - `streamChatResponse()` - Manages chat streaming lifecycle - `streamRegenerateResponse()` - Handles message regeneration #### `useConversationFlow.ts` Manages the main conversation flow and user interactions. **Features:** - Input validation with comprehensive error handling - Proper message state management - Integrated error handling and user feedback - Type-safe message structures **Key Functions:** - `onConversationStream()` - Main conversation handler - `validateConversationInput()` - Input validation with user feedback - `addUserMessage()` - Adds user messages to chat - `initializeChatResponse()` - Sets up response placeholders #### `useRegenerate.ts` Handles message regeneration functionality. **Features:** - Smart regeneration context handling - Proper cleanup of existing messages - Error handling for regeneration failures - Support for both user and AI message regeneration **Key Functions:** - `onRegenerate()` - Main regeneration handler - `prepareRegenerateContext()` - Context setup for regeneration - `handleUserMessageRegenerate()` - User message regeneration logic #### `useSearchAndPrompts.ts` Manages search functionality and prompt templates. **Features:** - Debounced search for better performance - Memoized search results to prevent unnecessary computations - Type-safe search options and filtering - Support for both session and prompt searching **Key Functions:** - `searchOptions` - Computed search results with performance optimizations - `renderOption()` - Renders search option labels - `handleSelectAutoComplete()` - Handles search selection #### `useChatActions.ts` Contains various chat-related actions and utilities. **Features:** - Snapshot and bot creation functionality - File upload handling - Gallery and modal management - VFS (Virtual File System) integration **Key Functions:** - `handleSnapshot()` - Creates chat snapshots - `handleCreateBot()` - Bot creation functionality - `handleVFSFileUploaded()` - File upload handling - `toggleArtifactGallery()` - UI state management ### Utility Composables #### `useErrorHandling.ts` Centralized error management system. **Features:** - Comprehensive error logging and tracking - User-friendly error notifications - API error handling with proper HTTP status mapping - Retry mechanism with exponential backoff - Error history management **Key Functions:** - `handleApiError()` - Handles API errors with proper classification - `logError()` - Logs errors with context and timestamp - `retryOperation()` - Retry mechanism for failed operations - `showErrorNotification()` - User notifications #### `useValidation.ts` Input validation system with reusable rules. **Features:** - Comprehensive validation rules (required, length, email, URL, etc.) - Form field validation with reactive state - Custom validation rule support - Specific validators for chat messages, UUIDs, and file uploads **Key Functions:** - `validateChatMessage()` - Chat message validation - `validateSessionUuid()` - UUID format validation - `validateFileUpload()` - File validation with size and type checks - `useField()` - Reactive form field validation #### `usePerformanceOptimizations.ts` Performance optimization utilities. **Features:** - Debouncing for high-frequency inputs - Memoization for expensive computations - Virtual scrolling for large lists - Throttling for event handlers **Key Functions:** - `useDebounce()` - Debounces reactive values - `useMemoized()` - Memoizes expensive computations - `useVirtualList()` - Virtual scrolling implementation - `useThrottle()` - Throttles function calls ## Usage Examples ### Basic Usage in Components ```typescript // In a Vue component import { useConversationFlow } from './composables/useConversationFlow' import { useErrorHandling } from './composables/useErrorHandling' export default { setup() { const sessionUuid = 'your-session-uuid' const conversationFlow = useConversationFlow(sessionUuid) const { showErrorNotification } = useErrorHandling() const handleSubmit = async (message: string) => { try { await conversationFlow.onConversationStream(message, dataSources.value) } catch (error) { showErrorNotification('Failed to send message') } } return { handleSubmit, loading: conversationFlow.loading } } } ``` ### Validation Example ```typescript import { useValidation } from './composables/useValidation' const { useField, rules } = useValidation() const messageField = useField('', [ rules.required('Message is required'), rules.maxLength(1000, 'Message too long') ]) // Use in template // v-model="messageField.value.value" // :error="messageField.showErrors.value" ``` ### Performance Optimization Example ```typescript import { useDebounce, useMemoized } from './composables/usePerformanceOptimizations' const searchTerm = ref('') const debouncedSearch = useDebounce(searchTerm, 300) const expensiveComputation = useMemoized( (data) => computeHeavyCalculation(data), () => someReactiveData.value ) ``` ## Benefits of This Refactoring ### 1. **Separation of Concerns** Each composable has a single responsibility, making the code easier to understand and maintain. ### 2. **Reusability** Composables can be reused across different components, reducing code duplication. ### 3. **Testability** Individual composables can be tested in isolation, improving test coverage and reliability. ### 4. **Type Safety** Comprehensive TypeScript interfaces and types provide better development experience and catch errors at compile time. ### 5. **Performance** Optimizations like debouncing, memoization, and proper error handling improve the overall user experience. ### 6. **Error Handling** Centralized error management provides consistent error handling across the application. ### 7. **Maintainability** Smaller, focused files are easier to maintain and update. ## File Size Reduction The main `Conversation.vue` file was reduced from **738 lines** to **293 lines** (60% reduction) while gaining: - Better error handling - Performance optimizations - Type safety - Improved reusability - Enhanced maintainability ## Future Improvements 1. **Unit Tests**: Add comprehensive unit tests for each composable 2. **Documentation**: Add JSDoc comments to all public functions 3. **Logging**: Integrate with application logging system 4. **Metrics**: Add performance metrics collection 5. **Accessibility**: Enhance accessibility features 6. **Internationalization**: Improve i18n support throughout composables ================================================ FILE: web/src/views/chat/composables/useChatActions.ts ================================================ import { ref, type Ref } from 'vue' import { useDialog, useMessage } from 'naive-ui' import { v4 as uuidv4 } from 'uuid' import { createChatBot, createChatSnapshot, getChatSessionDefault } from '@/api' import { useAppStore, useSessionStore, useMessageStore } from '@/store' import { useBasicLayout } from '@/hooks/useBasicLayout' import { useChat } from '@/views/chat/hooks/useChat' import { nowISO } from '@/utils/date' import { extractArtifacts } from '@/utils/artifacts' import { t } from '@/locales' export function useChatActions(sessionUuidRef: Ref) { const dialog = useDialog() const nui_msg = useMessage() const sessionStore = useSessionStore() const messageStore = useMessageStore() const appStore = useAppStore() const { isMobile } = useBasicLayout() const { addChat } = useChat() const snapshotLoading = ref(false) const botLoading = ref(false) const showUploadModal = ref(false) const showModal = ref(false) const showArtifactGallery = ref(false) async function handleAdd(dataSources: any[]) { if (dataSources.length > 0) { const new_chat_text = t('chat.new') try { await sessionStore.createNewSession(new_chat_text) if (isMobile.value) appStore.setSiderCollapsed(true) } catch (error) { console.error('Failed to create new session:', error) } } else { nui_msg.warning(t('chat.alreadyInNewChat')) } } async function handleSnapshot() { const sessionUuid = sessionUuidRef.value if (!sessionUuid) { nui_msg.error('No active session selected.') return } snapshotLoading.value = true try { const snapshot = await createChatSnapshot(sessionUuid) const snapshot_uuid = snapshot.uuid window.open(`#/snapshot/${snapshot_uuid}`, '_blank') nui_msg.success(t('chat.snapshotSuccess')) } catch (error) { nui_msg.error(t('chat.snapshotFailed')) } finally { snapshotLoading.value = false } } async function handleCreateBot() { const sessionUuid = sessionUuidRef.value if (!sessionUuid) { nui_msg.error('No active session selected.') return } botLoading.value = true try { const snapshot = await createChatBot(sessionUuid) const snapshot_uuid = snapshot.uuid window.open(`#/snapshot/${snapshot_uuid}`, '_blank') nui_msg.success(t('chat.botSuccess')) } catch (error) { nui_msg.error(t('chat.botFailed')) } finally { botLoading.value = false } } function handleClear(loading: any) { if (loading.value) return const sessionUuid = sessionUuidRef.value if (!sessionUuid) { nui_msg.error('No active session selected.') return } console.log('🔄 handleClear called with sessionUuid:', sessionUuid) dialog.warning({ title: t('chat.clearChat'), content: t('chat.clearChatConfirm'), positiveText: t('common.yes'), negativeText: t('common.no'), onPositiveClick: () => { console.log('🔄 Clearing messages for sessionUuid:', sessionUuid) messageStore.clearSessionMessages(sessionUuid) }, }) } const toggleArtifactGallery = (): void => { showArtifactGallery.value = !showArtifactGallery.value } const handleVFSFileUploaded = (fileInfo: any) => { nui_msg.success(`📁 File uploaded: ${fileInfo.filename}`) } const handleCodeExampleAdded = async (codeInfo: any, streamResponse: any) => { const sessionUuid = sessionUuidRef.value if (!sessionUuid) { nui_msg.error('No active session selected.') return } const exampleMessage = `📁 **Files uploaded successfully!** **Python example:** \`\`\`python ${codeInfo.python} \`\`\` **JavaScript example:** \`\`\`javascript ${codeInfo.javascript} \`\`\` Your files are now available in the Virtual File System! 🚀` const chatUuid = uuidv4() addChat( sessionUuid, { uuid: chatUuid, dateTime: nowISO(), text: exampleMessage, inversion: true, error: false, loading: false, artifacts: extractArtifacts(exampleMessage), }, ) try { await streamResponse(chatUuid, exampleMessage) nui_msg.success('Files uploaded! Code examples added to chat.') } catch (error) { console.error('Failed to stream code example response:', error) } } return { snapshotLoading, botLoading, showUploadModal, showModal, showArtifactGallery, handleAdd, handleSnapshot, handleCreateBot, handleClear, toggleArtifactGallery, handleVFSFileUploaded, handleCodeExampleAdded } } ================================================ FILE: web/src/views/chat/composables/useConversationFlow.ts ================================================ import { type Ref, ref } from 'vue' import { v7 as uuidv7 } from 'uuid' import { useStreamHandling } from './useStreamHandling' import { useErrorHandling } from './useErrorHandling' import { useValidation } from './useValidation' import { useChat } from '@/views/chat/hooks/useChat' import { nowISO } from '@/utils/date' import { useMessageStore, useSessionStore } from '@/store' interface ChatMessage { uuid: string dateTime: string text: string inversion: boolean error: boolean loading?: boolean artifacts?: any[] } export function useConversationFlow( sessionUuidRef: Ref, scrollToBottom: () => Promise, smoothScrollToBottomIfAtBottom: () => Promise, ) { const loading = ref(false) const abortController = ref(null) const { addChat, updateChat, updateChatPartial } = useChat() const { streamChatResponse, processStreamChunk } = useStreamHandling() const { handleApiError, showErrorNotification } = useErrorHandling() const { validateChatMessage } = useValidation() const sessionStore = useSessionStore() const messageStore = useMessageStore() async function refreshSessionTitle(sessionUuid: string): Promise { const session = sessionStore.getChatSessionByUuid(sessionUuid) const workspaceUuid = session?.workspaceUuid if (!workspaceUuid) return const maxAttempts = 8 const retryDelayMs = 2000 let lastSeenTitle = session.title for (let attempt = 0; attempt < maxAttempts; attempt++) { if (attempt > 0) await new Promise(resolve => setTimeout(resolve, retryDelayMs)) await sessionStore.syncWorkspaceSessions(workspaceUuid) const refreshedSession = sessionStore.getChatSessionByUuid(sessionUuid) if (!refreshedSession?.title) return if (refreshedSession.title !== lastSeenTitle) return lastSeenTitle = refreshedSession.title } } function validateConversationInput(message: string): boolean { if (loading.value) { showErrorNotification('Please wait for the current message to complete') return false } if (!sessionUuidRef.value) { showErrorNotification('No active session selected') return false } // Validate message content const messageValidation = validateChatMessage(message) if (!messageValidation.isValid) { showErrorNotification(messageValidation.errors[0]) return false } return true } async function addUserMessage(chatUuid: string, message: string): Promise { const sessionUuid = sessionUuidRef.value if (!sessionUuid) return const existingMessages = messageStore.getChatSessionDataByUuid(sessionUuid) const onlySystemPromptPresent = existingMessages.length === 1 && existingMessages[0]?.isPrompt === true // For sessions that currently only have the system prompt, backend treats // the first input as prompt content. Skip adding a duplicated user bubble. if (onlySystemPromptPresent) return const chatMessage: ChatMessage = { uuid: chatUuid, dateTime: nowISO(), text: message, inversion: true, error: false, } addChat(sessionUuid, chatMessage) await scrollToBottom() } async function initializeChatResponse(dataSources: any[]): Promise { const sessionUuid = sessionUuidRef.value if (!sessionUuid) return dataSources.length - 1 addChat(sessionUuid, { uuid: '', dateTime: nowISO(), text: '', loading: true, inversion: false, error: false, }) await smoothScrollToBottomIfAtBottom() return dataSources.length - 1 } function handleStreamingError(error: any, responseIndex: number, dataSources: any[]): void { handleApiError(error, 'conversation-stream') const lastMessage = dataSources[responseIndex] const sessionUuid = sessionUuidRef.value if (!sessionUuid) return if (lastMessage) { const errorMessage: ChatMessage = { uuid: lastMessage.uuid || uuidv7(), dateTime: nowISO(), text: 'Failed to get response. Please try again.', inversion: false, error: true, loading: false, } updateChat(sessionUuid, responseIndex, errorMessage) } } function stopStream(): void { if (abortController.value) { abortController.value.abort() abortController.value = null loading.value = false } } async function startStream( message: string, dataSources: any[], chatUuid: string, ): Promise { const sessionUuid = sessionUuidRef.value if (!sessionUuid) { loading.value = false abortController.value = null return } loading.value = true abortController.value = new AbortController() const responseIndex = await initializeChatResponse(dataSources) try { await streamChatResponse( sessionUuid, chatUuid, message, responseIndex, async (chunk: string, index: number) => { processStreamChunk(chunk, index, sessionUuid) await smoothScrollToBottomIfAtBottom() }, abortController.value.signal, ) } catch (error) { if (error instanceof Error && error.name === 'AbortError') { // Stream was cancelled, no need to show error return } handleStreamingError(error, responseIndex, dataSources) } finally { loading.value = false abortController.value = null try { await refreshSessionTitle(sessionUuid) } catch (error) { console.error('Failed to refresh session title:', error) } // For sessions in exploreMode, set suggested questions loading state const session = sessionStore.getChatSessionByUuid(sessionUuid) if (session?.exploreMode && dataSources[responseIndex] && !dataSources[responseIndex].inversion) { updateChatPartial(sessionUuid, responseIndex, { suggestedQuestionsLoading: true, }) } } } return { loading, validateConversationInput, addUserMessage, initializeChatResponse, handleStreamingError, startStream, stopStream, } } ================================================ FILE: web/src/views/chat/composables/useErrorHandling.ts ================================================ import { ref, computed } from 'vue' import { useMessage } from 'naive-ui' import { t } from '@/locales' import { useAuthStore } from '@/store' import * as notificationManager from '@/utils/notificationManager' interface AppError { code?: number | string message: string details?: any timestamp: Date context?: string } interface ErrorState { hasError: boolean currentError: AppError | null errorHistory: AppError[] } export function useErrorHandling() { const nui_msg = useMessage() const errorState = ref({ hasError: false, currentError: null, errorHistory: [] }) const hasRecentErrors = computed(() => { const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000) return errorState.value.errorHistory.some(error => error.timestamp > fiveMinutesAgo) }) const errorCount = computed(() => errorState.value.errorHistory.length) function getErrorTitle(errorType: string, errorCode: string | number): string { switch (errorType) { case 'network': return 'Connection Problem' case 'server': return errorCode >= 500 ? 'Server Error' : 'Request Failed' case 'auth': return 'Authentication Required' case 'timeout': return 'Request Timeout' default: return 'Error' } } function logError(error: Partial, context?: string): void { const appError: AppError = { code: error.code, message: error.message || 'Unknown error occurred', details: error.details, timestamp: new Date(), context: context || 'general' } errorState.value.errorHistory.push(appError) errorState.value.currentError = appError errorState.value.hasError = true // Limit error history to prevent memory leaks if (errorState.value.errorHistory.length > 50) { errorState.value.errorHistory.shift() } console.error(`[${context}] Error:`, appError) } function handleApiError(error: any, context: string = 'api'): void { let errorMessage = 'An unexpected error occurred' let errorCode: string | number = 'UNKNOWN' let errorType: 'network' | 'auth' | 'server' | 'client' | 'timeout' | 'unknown' = 'unknown' let action: { text: string; onClick: () => void } | undefined if (error?.response) { // HTTP error response errorCode = error.response.status errorMessage = error.response.data?.message || `HTTP ${error.response.status}` if (error.response.status === 401) { errorMessage = t('error.unauthorized') || 'Session expired. Please login again.' errorType = 'auth' action = { text: 'Login', onClick: () => { const authStore = useAuthStore() authStore.removeToken() authStore.removeExpiresIn() } } } else if (error.response.status === 403) { errorMessage = t('error.forbidden') || 'Access denied. You don\'t have permission for this action.' errorType = 'auth' } else if (error.response.status === 404) { errorMessage = t('error.notFound') || 'The requested resource was not found.' errorType = 'client' } else if (error.response.status === 429) { errorMessage = 'Too many requests. Please wait a moment before trying again.' errorType = 'client' action = { text: 'Retry', onClick: () => window.location.reload() } } else if (error.response.status >= 500) { errorMessage = t('error.serverError') || 'Server error. Our team has been notified and is working on a fix.' errorType = 'server' action = { text: 'Retry', onClick: () => window.location.reload() } } else { errorType = 'client' } } else if (error?.message) { // Network or other errors errorMessage = error.message if (error.message.includes('timeout') || error.message.includes('TIMEOUT')) { errorMessage = t('error.timeout') || 'Request timed out. Please check your connection and try again.' errorType = 'timeout' action = { text: 'Retry', onClick: () => window.location.reload() } } else if (error.message.includes('network') || error.message.includes('Network Error') || error.code === 'ECONNABORTED') { errorMessage = t('error.network') || 'Network connection error. Please check your internet connection.' errorType = 'network' action = { text: 'Retry', onClick: () => window.location.reload() } } } else if (error?.code === 'ERR_CANCELED') { errorMessage = 'Request was cancelled.' errorType = 'client' return // Don't show notification for cancelled requests } logError({ code: errorCode, message: errorMessage, details: error }, context) // Use enhanced notifications for better visual hierarchy if (errorType === 'server' || errorType === 'network') { notificationManager.showEnhancedErrorNotification( getErrorTitle(errorType, errorCode), errorMessage, { persistent: true, action } ) } else if (errorType === 'auth') { notificationManager.showEnhancedWarningNotification( 'Authentication Required', errorMessage, { persistent: true, action } ) } else { notificationManager.showEnhancedErrorNotification( 'Error', errorMessage, { duration: 5000, action } ) } } function handleStreamError(responseText: string, context: string = 'stream'): void { try { const errorJson = JSON.parse(responseText) const errorMessage = t(`error.${errorJson.code}`) || errorJson.message || 'Stream error occurred' logError({ code: errorJson.code, message: errorMessage, details: errorJson.details }, context) // Handle specific stream errors with better messages let action: { text: string; onClick: () => void } | undefined if (errorJson.code === 'MODEL_006' || errorJson.code === 'INTN_004') { action = { text: 'Retry', onClick: () => window.location.reload() } } notificationManager.showEnhancedErrorNotification( 'Stream Error', errorMessage, { duration: 5000, action } ) } catch (parseError) { logError({ message: 'Failed to parse error response', details: { responseText, parseError } }, context) notificationManager.showEnhancedErrorNotification( 'Connection Error', 'Connection interrupted. Please check your connection and try again.', { persistent: true, action: { text: 'Retry', onClick: () => window.location.reload() } } ) } } function showErrorNotification(message: string, duration: number = 5000, action?: { text: string; onClick: () => void }): void { notificationManager.showErrorNotification(message, { duration, action }) } function showWarningNotification(message: string, duration: number = 3000, action?: { text: string; onClick: () => void }): void { notificationManager.showWarningNotification(message, { duration, action }) } function showSuccessNotification(message: string, duration: number = 3000): void { notificationManager.showSuccessNotification(message, { duration }) } function showInfoNotification(message: string, duration: number = 3000): void { notificationManager.showInfoNotification(message, { duration }) } function showPersistentErrorNotification(message: string, action?: { text: string; onClick: () => void }): void { notificationManager.showPersistentNotification(message, 'error', action) } function clearError(): void { errorState.value.hasError = false errorState.value.currentError = null } function clearErrorHistory(): void { errorState.value.errorHistory = [] clearError() } function retryOperation( operation: () => Promise, maxRetries: number = 3, delay: number = 1000 ): Promise { return new Promise(async (resolve, reject) => { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = await operation() resolve(result) return } catch (error) { if (attempt === maxRetries) { handleApiError(error, 'retry-operation') reject(error) return } // Show retry notification if (attempt === 1) { showWarningNotification(`Retrying... (${attempt}/${maxRetries})`, 2000) } // Wait before retrying await new Promise(resolve => setTimeout(resolve, delay * attempt)) } } }) } function showNetworkStatusNotification(): void { if (!navigator.onLine) { showPersistentErrorNotification('You are offline. Please check your internet connection.', { text: 'Retry', onClick: () => window.location.reload() }) } } function clearAllNotifications(): void { notificationManager.clearAllNotifications() } return { errorState: computed(() => errorState.value), hasRecentErrors, errorCount, logError, handleApiError, handleStreamError, showErrorNotification, showWarningNotification, showSuccessNotification, showInfoNotification, showPersistentErrorNotification, clearError, clearErrorHistory, retryOperation, showNetworkStatusNotification, clearAllNotifications } } ================================================ FILE: web/src/views/chat/composables/usePerformanceOptimizations.ts ================================================ import { ref, computed, watch, type Ref } from 'vue' /** * Debounce utility for search input */ export function useDebounce(value: Ref | T, delay: number) { // Handle both refs and raw values const isRef = value && typeof value === 'object' && '__v_isRef' in value const initialValue = isRef ? (value as Ref).value : value as T const debouncedValue = ref(initialValue) let timeoutId: NodeJS.Timeout if (isRef) { // If it's a ref, watch the ref directly watch(value as Ref, (newValue) => { clearTimeout(timeoutId) timeoutId = setTimeout(() => { debouncedValue.value = newValue }, delay) }, { immediate: true }) } else { // If it's a raw value, watch it as a getter watch(() => value, (newValue) => { clearTimeout(timeoutId) timeoutId = setTimeout(() => { debouncedValue.value = newValue as T }, delay) }, { immediate: true }) } return debouncedValue } /** * Virtual scrolling helper for large lists */ export function useVirtualList( items: T[], itemHeight: number, containerHeight: number ) { const scrollTop = ref(0) const visibleItems = computed(() => { const startIndex = Math.floor(scrollTop.value / itemHeight) const endIndex = Math.min( startIndex + Math.ceil(containerHeight / itemHeight) + 1, items.length ) return { startIndex, endIndex, items: items.slice(startIndex, endIndex), offsetY: startIndex * itemHeight, totalHeight: items.length * itemHeight } }) const handleScroll = (event: Event) => { const target = event.target as HTMLElement scrollTop.value = target.scrollTop } return { visibleItems, handleScroll } } /** * Throttle utility for high-frequency events */ export function useThrottle any>( fn: T, delay: number ): T { let lastCall = 0 return ((...args: Parameters) => { const now = Date.now() if (now - lastCall >= delay) { lastCall = now return fn(...args) } }) as T } ================================================ FILE: web/src/views/chat/composables/useRegenerate.ts ================================================ import { ref, type Ref } from 'vue' import { deleteChatMessage } from '@/api' import { nowISO } from '@/utils/date' import { useChat } from '@/views/chat/hooks/useChat' import { useStreamHandling } from './useStreamHandling' import { t } from '@/locales' export function useRegenerate(sessionUuidRef: Ref) { const loading = ref(false) const abortController = ref(null) const { addChat, updateChat, updateChatPartial } = useChat() const { streamRegenerateResponse, processStreamChunk } = useStreamHandling() function validateRegenerateInput(): boolean { return !loading.value } async function prepareRegenerateContext( index: number, chat: any, dataSources: any[] ): Promise<{ updateIndex: number; isRegenerate: boolean }> { const sessionUuid = sessionUuidRef.value if (!sessionUuid) { return { updateIndex: index, isRegenerate: true } } loading.value = true let updateIndex = index let isRegenerate = true if (chat.inversion) { const result = await handleUserMessageRegenerate(index, dataSources) updateIndex = result.updateIndex isRegenerate = result.isRegenerate } else { updateChat(sessionUuid, index, { uuid: chat.uuid, dateTime: nowISO(), text: '', inversion: false, error: false, loading: true, suggestedQuestionsLoading: true, }) } return { updateIndex, isRegenerate } } async function handleUserMessageRegenerate( index: number, dataSources: any[] ): Promise<{ updateIndex: number; isRegenerate: boolean }> { const sessionUuid = sessionUuidRef.value if (!sessionUuid) { return { updateIndex: index, isRegenerate: false } } const chatNext = dataSources[index + 1] let updateIndex = index + 1 const isRegenerate = false if (chatNext) { await deleteChatMessage(chatNext.uuid) updateChat(sessionUuid, updateIndex, { uuid: chatNext.uuid, dateTime: nowISO(), text: '', inversion: false, error: false, loading: true, suggestedQuestionsLoading: true, }) } else { addChat(sessionUuid, { uuid: '', dateTime: nowISO(), text: '', loading: true, inversion: false, error: false, suggestedQuestionsLoading: true, }) } return { updateIndex, isRegenerate } } function handleRegenerateError(error: any, chatUuid: string, index: number): void { const sessionUuid = sessionUuidRef.value if (!sessionUuid) { return } console.error('Regenerate error:', error) if (error.message === 'canceled') { updateChatPartial(sessionUuid, index, { loading: false, }) return } const errorMessage = error?.message ?? t('common.wrong') updateChat(sessionUuid, index, { uuid: chatUuid, dateTime: nowISO(), text: errorMessage, inversion: false, error: true, loading: false, }) } function stopRegenerate(): void { if (abortController.value) { abortController.value.abort() abortController.value = null loading.value = false } } async function onRegenerate(index: number, dataSources: any[]): Promise { if (!validateRegenerateInput()) return const sessionUuid = sessionUuidRef.value if (!sessionUuid) { return } const chat = dataSources[index] abortController.value = new AbortController() const { updateIndex, isRegenerate } = await prepareRegenerateContext(index, chat, dataSources) try { await streamRegenerateResponse( sessionUuid, chat.uuid, updateIndex, isRegenerate, (chunk: string, updateIdx: number) => { processStreamChunk(chunk, updateIdx, sessionUuid) }, abortController.value.signal ) } catch (error) { if (error instanceof Error && error.name === 'AbortError') { // Stream was cancelled, no need to show error return } handleRegenerateError(error, chat.uuid, index) } finally { loading.value = false abortController.value = null } } return { loading, validateRegenerateInput, prepareRegenerateContext, handleUserMessageRegenerate, handleRegenerateError, onRegenerate, stopRegenerate } } ================================================ FILE: web/src/views/chat/composables/useSearchAndPrompts.ts ================================================ import { computed, ref } from 'vue' import { storeToRefs } from 'pinia' import { type OnSelect } from 'naive-ui/es/auto-complete/src/interface' import { useSessionStore, usePromptStore } from '@/store' import { useDebounce } from './usePerformanceOptimizations' interface PromptItem { key: string value: string } interface ChatItem { uuid: string title: string } interface SearchOption { label: string value: string } export function useSearchAndPrompts() { const prompt = ref('') const sessionStore = useSessionStore() const promptStore = usePromptStore() // Get reactive store refs without explicit typing to avoid type issues const storeRefs = storeToRefs(promptStore) const promptTemplate = computed(() => storeRefs.promptList?.value || []) // Debounce search input for better performance const debouncedPrompt = useDebounce(prompt, 300) // Search options computed directly - much simpler! const searchOptions = computed((): SearchOption[] => { let searchPrompt = debouncedPrompt.value // Ensure searchPrompt is a string if (typeof searchPrompt !== 'string') { console.warn('debouncedPrompt.value is not a string:', typeof searchPrompt, searchPrompt) searchPrompt = String(searchPrompt || '') } if (!searchPrompt.startsWith('/')) { return [] } const filterItemsByPrompt = (item: PromptItem): boolean => { const lowerCaseKey = item.key.toLowerCase() const lowerCasePrompt = searchPrompt.substring(1).toLowerCase() return lowerCaseKey.includes(lowerCasePrompt) } const filterItemsByTitle = (item: ChatItem): boolean => { const lowerCaseTitle = item.title.toLowerCase() const lowerCasePrompt = searchPrompt.substring(1).toLowerCase() return lowerCaseTitle.includes(lowerCasePrompt) } // Get all sessions from workspace history const allSessions: ChatItem[] = [] for (const sessions of Object.values(sessionStore.workspaceHistory)) { allSessions.push(...sessions) } const sessionOptions: SearchOption[] = allSessions .filter(filterItemsByTitle) .map((session: ChatItem) => ({ label: `UUID|$|${session.uuid}`, value: `UUID|$|${session.uuid}`, })) const promptOptions: SearchOption[] = promptTemplate.value .filter(filterItemsByPrompt) .map((item: PromptItem) => ({ label: item.value, value: item.value, })) return [...sessionOptions, ...promptOptions] }) const renderOption = (option: { label: string }): string[] => { // Check if it's a prompt template const promptItem = promptTemplate.value.find((item: PromptItem) => item.value === option.label) if (promptItem) { return [promptItem.key] } // Check if it's a chat session across all workspace histories let chatItem = null for (const sessions of Object.values(sessionStore.workspaceHistory)) { chatItem = sessions.find((chat: ChatItem) => `UUID|$|${chat.uuid}` === option.label) if (chatItem) break } if (chatItem) { return [chatItem.title] } return [] } const handleSelectAutoComplete: OnSelect = function (v: string | number) { if (typeof v === 'string' && v.startsWith('UUID|$|')) { const sessionUuid = v.split('|$|')[1] const session = sessionStore.getChatSessionByUuid(sessionUuid) if (session && session.workspaceUuid) { // Switch to the workspace and session sessionStore.setActiveSession(session.workspaceUuid, sessionUuid) } } } const handleUsePrompt = (_: string, value: string): void => { prompt.value = value } return { prompt, searchOptions, renderOption, handleSelectAutoComplete, handleUsePrompt } } ================================================ FILE: web/src/views/chat/composables/useStreamHandling.ts ================================================ import { useMessage } from 'naive-ui' import { useAuthStore, useMessageStore } from '@/store' import { extractStreamingData } from '@/utils/string' import { extractArtifacts } from '@/utils/artifacts' import { nowISO } from '@/utils/date' import { useChat } from '@/views/chat/hooks/useChat' import renderMessage from '../components/RenderMessage.vue' import { t } from '@/locales' import { getStreamingUrl } from '@/config/api' interface ErrorResponse { code: number message: string details?: any } interface StreamChunkData { choices: Array<{ delta: { content: string suggestedQuestions?: string[] } }> id: string } export function useStreamHandling() { const nui_msg = useMessage() const messageStore = useMessageStore() const { updateChat } = useChat() function handleStreamError(responseText: string, responseIndex: number, sessionUuid: string): void { try { const errorJson: ErrorResponse = JSON.parse(responseText) console.error('Stream error:', errorJson) const errorMessage = formatErr(errorJson) nui_msg.error(errorMessage, { duration: 5000, closable: true, render: renderMessage }) const messages = messageStore.getChatSessionDataByUuid(sessionUuid) if (messages && messages[responseIndex]) { messageStore.removeMessage(sessionUuid, messages[responseIndex].uuid) } } catch (parseError) { console.error('Failed to parse error response:', parseError) nui_msg.error('An unexpected error occurred') } } function processStreamChunk(chunk: string, responseIndex: number, sessionUuid: string): void { const data = extractStreamingData(chunk) if (!data) return try { const parsedData: StreamChunkData = JSON.parse(data) const delta = parsedData.choices?.[0]?.delta const answerUuid = parsedData.id?.replace('chatcmpl-', '') || parsedData.id // Handle both content and suggested questions const deltaContent = delta?.content || '' const suggestedQuestions = delta?.suggestedQuestions // Skip if neither content nor suggested questions are present if (!deltaContent && !suggestedQuestions && !parsedData.id) { console.warn('Invalid stream chunk structure:', parsedData) return } // Get current message const messages = messageStore.getChatSessionDataByUuid(sessionUuid) const currentMessage = messages && messages[responseIndex] ? messages[responseIndex] : null // Process content if present let newText = currentMessage?.text || '' let artifacts = currentMessage?.artifacts || [] if (deltaContent) { newText = newText + deltaContent artifacts = extractArtifacts(newText) } // Prepare update object const updateData: any = { uuid: answerUuid, dateTime: nowISO(), text: newText, inversion: false, error: false, loading: false, artifacts: artifacts, } // Add suggested questions if present if (suggestedQuestions && Array.isArray(suggestedQuestions) && suggestedQuestions.length > 0) { updateData.suggestedQuestions = suggestedQuestions updateData.suggestedQuestionsLoading = false // Clear loading state when questions are received console.log('Received suggested questions via stream:', suggestedQuestions) } updateChat(sessionUuid, responseIndex, updateData) } catch (error) { console.error('Failed to parse stream chunk:', error) } } async function streamChatResponse( sessionUuid: string, chatUuid: string, message: string, responseIndex: number, onStreamChunk: (chunk: string, responseIndex: number) => void, abortSignal?: AbortSignal ): Promise { const authStore = useAuthStore() console.log('authStore', authStore.isValid) await authStore.initializeAuth() if (!authStore.isValid || authStore.needsRefresh) { try { await authStore.refreshToken() } catch (error) { authStore.removeToken() authStore.removeExpiresIn() nui_msg.error(t('error.NotAuthorized') || 'Please log in first', { duration: 5000, closable: true, render: renderMessage }) return } } const token = authStore.getToken try { const response = await fetch(getStreamingUrl('/chat_stream'), { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', ...(token && { 'Authorization': `Bearer ${token}` }), }, body: JSON.stringify({ regenerate: false, prompt: message, sessionUuid, chatUuid, stream: true, }), signal: abortSignal, }) if (!response.ok) { const errorText = await response.text() handleStreamError(errorText, responseIndex, sessionUuid) return } if (!response.body) { throw new Error('Response body is null') } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' try { while (true) { const { done, value } = await reader.read() if (done) { break } const chunk = decoder.decode(value, { stream: true }) console.log('chunk', chunk) buffer += chunk // Process complete SSE messages const lines = buffer.split('\n\n') // Keep the last potentially incomplete message in buffer buffer = lines.pop() || '' for (const line of lines) { if (line.trim()) { onStreamChunk(line, responseIndex) } } } // Process any remaining data in buffer if (buffer.trim()) { onStreamChunk(buffer, responseIndex) } } finally { reader.releaseLock() } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.log('Stream was cancelled by user') return } console.error('Stream error:', error) handleStreamError(error instanceof Error ? error.message : 'Unknown error', responseIndex, sessionUuid) throw error } } async function streamRegenerateResponse( sessionUuid: string, chatUuid: string, updateIndex: number, isRegenerate: boolean, onStreamChunk: (chunk: string, updateIndex: number) => void, abortSignal?: AbortSignal ): Promise { const authStore = useAuthStore() await authStore.initializeAuth() if (!authStore.isValid || authStore.needsRefresh) { try { await authStore.refreshToken() } catch (error) { authStore.removeToken() authStore.removeExpiresIn() nui_msg.error(t('error.NotAuthorized') || 'Please log in first', { duration: 5000, closable: true, render: renderMessage }) return } } const token = authStore.getToken try { const response = await fetch(getStreamingUrl('/chat_stream'), { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', ...(token && { 'Authorization': `Bearer ${token}` }), }, body: JSON.stringify({ regenerate: isRegenerate, prompt: "", sessionUuid, chatUuid, stream: true, }), signal: abortSignal, }) if (!response.ok) { const errorText = await response.text() handleStreamError(errorText, updateIndex, sessionUuid) return } if (!response.body) { throw new Error('Response body is null') } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' try { while (true) { const { done, value } = await reader.read() if (done) { break } const chunk = decoder.decode(value, { stream: true }) buffer += chunk // Process complete SSE messages const lines = buffer.split('\n\n') // Keep the last potentially incomplete message in buffer buffer = lines.pop() || '' for (const line of lines) { if (line.trim()) { onStreamChunk(line, updateIndex) } } } // Process any remaining data in buffer if (buffer.trim()) { onStreamChunk(buffer, updateIndex) } } finally { reader.releaseLock() } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.log('Regenerate stream was cancelled by user') return } console.error('Stream error:', error) handleStreamError(error instanceof Error ? error.message : 'Unknown error', updateIndex, sessionUuid) throw error } } function formatErr(error_json: ErrorResponse): string { const message = t(`error.${error_json.code}`) || error_json.message return `${error_json.code}: ${message}` } return { handleStreamError, processStreamChunk, streamChatResponse, streamRegenerateResponse, formatErr } } ================================================ FILE: web/src/views/chat/composables/useValidation.ts ================================================ import { ref, computed, watch } from 'vue' import { t } from '@/locales' interface ValidationRule { validator: (value: any) => boolean message: string } interface ValidationResult { isValid: boolean errors: string[] } export function useValidation() { const createValidator = (rules: ValidationRule[]) => { return (value: any): ValidationResult => { const errors: string[] = [] for (const rule of rules) { if (!rule.validator(value)) { errors.push(rule.message) } } return { isValid: errors.length === 0, errors } } } // Common validation rules const rules = { required: (message?: string): ValidationRule => ({ validator: (value: any) => { if (typeof value === 'string') return value.trim().length > 0 return value != null && value !== '' }, message: message || t('validation.required') || 'This field is required' }), minLength: (min: number, message?: string): ValidationRule => ({ validator: (value: string) => !value || value.length >= min, message: message || t('validation.minLength', { min }) || `Minimum length is ${min} characters` }), maxLength: (max: number, message?: string): ValidationRule => ({ validator: (value: string) => !value || value.length <= max, message: message || t('validation.maxLength', { max }) || `Maximum length is ${max} characters` }), email: (message?: string): ValidationRule => ({ validator: (value: string) => { if (!value) return true const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ return emailRegex.test(value) }, message: message || t('validation.email') || 'Please enter a valid email address' }), url: (message?: string): ValidationRule => ({ validator: (value: string) => { if (!value) return true try { new URL(value) return true } catch { return false } }, message: message || t('validation.url') || 'Please enter a valid URL' }), pattern: (regex: RegExp, message: string): ValidationRule => ({ validator: (value: string) => !value || regex.test(value), message }), custom: (validator: (value: any) => boolean, message: string): ValidationRule => ({ validator, message }) } // Form field validation function useField( initialValue: T, validationRules: ValidationRule[] = [] ) { const value = ref(initialValue) const touched = ref(false) const errors = ref([]) const validator = createValidator(validationRules) const isValid = computed(() => errors.value.length === 0) const hasErrors = computed(() => errors.value.length > 0) const showErrors = computed(() => touched.value && hasErrors.value) const validate = () => { const result = validator(value.value) errors.value = result.errors return result.isValid } const touch = () => { touched.value = true } const reset = () => { value.value = initialValue touched.value = false errors.value = [] } // Validate on value change watch(value, () => { if (touched.value) { validate() } }) return { value, errors: computed(() => errors.value), isValid, hasErrors, showErrors, validate, touch, reset } } // Chat message validation function validateChatMessage(message: string): ValidationResult { const messageRules = [ rules.required('Message cannot be empty'), rules.maxLength(10000, 'Message is too long (max 10,000 characters)') ] return createValidator(messageRules)(message) } // Session UUID validation function validateSessionUuid(uuid: string): ValidationResult { const uuidRules = [ rules.required('Session UUID is required'), rules.pattern( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, 'Invalid UUID format' ) ] return createValidator(uuidRules)(uuid) } // File upload validation function validateFileUpload( file: File, maxSize: number = 10 * 1024 * 1024, // 10MB allowedTypes: string[] = [] ): ValidationResult { const fileRules = [ rules.custom( () => file.size <= maxSize, `File size must be less than ${Math.round(maxSize / 1024 / 1024)}MB` ) ] if (allowedTypes.length > 0) { fileRules.push( rules.custom( () => allowedTypes.some(type => file.type.includes(type)), `File type not allowed. Allowed types: ${allowedTypes.join(', ')}` ) ) } return createValidator(fileRules)(file) } return { rules, createValidator, useField, validateChatMessage, validateSessionUuid, validateFileUpload } } ================================================ FILE: web/src/views/chat/hooks/useChat.ts ================================================ import { updateChatData } from '@/api' import { useMessageStore } from '@/store' import { nowISO } from '@/utils/date' export function useChat() { const messageStore = useMessageStore() const getChatByUuidAndIndex = (uuid: string, index: number) => { const messages = messageStore.getChatSessionDataByUuid(uuid) return messages && messages[index] ? messages[index] : null } const addChat = (uuid: string, chat: Chat.Message) => { messageStore.addMessage(uuid, chat) } const deleteChat = (uuid: string, index: number) => { const messages = messageStore.getChatSessionDataByUuid(uuid) if (messages && messages[index]) { messageStore.removeMessage(uuid, messages[index].uuid) } } const updateChat = (uuid: string, index: number, chat: Chat.Message) => { const messages = messageStore.getChatSessionDataByUuid(uuid) if (messages && messages[index]) { messageStore.updateMessage(uuid, messages[index].uuid, chat) } } const updateChatPartial = (uuid: string, index: number, chat: Partial) => { const messages = messageStore.getChatSessionDataByUuid(uuid) if (messages && messages[index]) { messageStore.updateMessage(uuid, messages[index].uuid, chat) } } const updateChatText = async (uuid: string, index: number, text: string) => { const messages = messageStore.getChatSessionDataByUuid(uuid) const chat = messages && messages[index] ? messages[index] : null if (!chat) return chat.text = text // update time stamp chat.dateTime = nowISO() messageStore.updateMessage(uuid, chat.uuid, chat) // sync text to server await updateChatData(chat) } return { addChat, deleteChat, updateChat, updateChatText, updateChatPartial, getChatByUuidAndIndex, } } ================================================ FILE: web/src/views/chat/hooks/useCopyCode.ts ================================================ import { onMounted, onUpdated } from 'vue' import { copyText } from '@/utils/format' export function useCopyCode() { function copyCodeBlock() { const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper') codeBlockWrapper.forEach((wrapper) => { const copyBtn = wrapper.querySelector('.code-block-header__copy') const codeBlock = wrapper.querySelector('.code-block-body') if (copyBtn && codeBlock) { copyBtn.addEventListener('click', () => { if (navigator.clipboard?.writeText) navigator.clipboard.writeText(codeBlock.textContent ?? '') else copyText({ text: codeBlock.textContent ?? '', origin: true }) }) } }) } onMounted(() => copyCodeBlock()) onUpdated(() => copyCodeBlock()) } ================================================ FILE: web/src/views/chat/hooks/useScroll.ts ================================================ import type { Ref } from 'vue' import { nextTick, ref, onUnmounted, watch } from 'vue' type ScrollElement = HTMLDivElement | null interface ScrollReturn { scrollRef: Ref scrollToBottom: () => Promise scrollToTop: () => Promise scrollToBottomIfAtBottom: () => Promise smoothScrollToBottomIfAtBottom: () => Promise } export function useScroll(): ScrollReturn { const scrollRef = ref(null) // State tracking for scroll behavior let isAutoScrolling = false let manualScrollTimeout: number | null = null let userHasManuallyScrolled = false let currentAnimation: number | null = null // Detect manual scrolling const handleScroll = () => { if (isAutoScrolling) return // Ignore scroll events during auto-scroll // Clear existing timeout if (manualScrollTimeout) { clearTimeout(manualScrollTimeout) } // Mark as manually scrolled userHasManuallyScrolled = true // Cancel any ongoing auto-scroll animation if (currentAnimation) { cancelAnimationFrame(currentAnimation) currentAnimation = null } // Reset manual scroll flag after user stops scrolling manualScrollTimeout = window.setTimeout(() => { userHasManuallyScrolled = false }, 2000) // 2 seconds of no scrolling } const scrollToBottom = async () => { await nextTick() if (scrollRef.value) { isAutoScrolling = true scrollRef.value.scrollTop = scrollRef.value.scrollHeight // Reset auto-scroll flag after a brief delay setTimeout(() => { isAutoScrolling = false }, 50) } } const scrollToTop = async () => { await nextTick() if (scrollRef.value) { isAutoScrolling = true scrollRef.value.scrollTop = 0 setTimeout(() => { isAutoScrolling = false }, 50) } } const scrollToBottomIfAtBottom = async () => { await nextTick() if (scrollRef.value && !userHasManuallyScrolled) { const element = scrollRef.value const threshold = Math.max(400, element.clientHeight * 0.25) // Dynamic threshold: 400px minimum or 25% of viewport const distanceToBottom = element.scrollHeight - element.scrollTop - element.clientHeight if (distanceToBottom <= threshold) { isAutoScrolling = true scrollRef.value.scrollTop = element.scrollHeight setTimeout(() => { isAutoScrolling = false }, 50) } } } const smoothScrollToBottomIfAtBottom = async () => { await nextTick() if (scrollRef.value && !userHasManuallyScrolled) { const element = scrollRef.value const threshold = Math.max(200, element.clientHeight * 0.1) // Smaller threshold: 200px minimum or 10% of viewport const distanceToBottom = element.scrollHeight - element.scrollTop - element.clientHeight if (distanceToBottom <= threshold) { // Cancel any existing animation to prevent conflicts if (currentAnimation) { cancelAnimationFrame(currentAnimation) currentAnimation = null } // Simple instant scroll to bottom without animation isAutoScrolling = true element.scrollTop = element.scrollHeight setTimeout(() => { isAutoScrolling = false }, 50) } } } // Setup event listener when scrollRef becomes available watch(scrollRef, (newElement, oldElement) => { // Remove listener from old element if (oldElement) { oldElement.removeEventListener('scroll', handleScroll) } // Add listener to new element if (newElement) { newElement.addEventListener('scroll', handleScroll, { passive: true }) } }, { immediate: true }) onUnmounted(() => { if (scrollRef.value) { scrollRef.value.removeEventListener('scroll', handleScroll) } if (manualScrollTimeout) { clearTimeout(manualScrollTimeout) } if (currentAnimation) { cancelAnimationFrame(currentAnimation) } }) return { scrollRef, scrollToBottom, scrollToTop, scrollToBottomIfAtBottom, smoothScrollToBottomIfAtBottom, } } ================================================ FILE: web/src/views/chat/hooks/useSlashToFocus.ts ================================================ // src/composables/useSlashToFocus.ts import { onMounted, onBeforeUnmount, type Ref, toRaw } from 'vue'; /** * Custom composable to intercept the '/' key globally and focus a target input. * * @param targetInputRef - A Vue ref pointing to the input element to focus. */ export function useSlashToFocus(targetInputRef: Ref): void { const handleGlobalKeyPress = (event: KeyboardEvent): void => { // Ensure the ref is pointing to an element if (!targetInputRef.value) { return; } const activeElement = document.activeElement; // This is already a raw element // Check if the pressed key is '/' if (event.key === '/') { // If the target input is already focused, allow the '/' to be typed // Compare the raw target element with the active element const isTypingInInput = activeElement && ( activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA'); if (isTypingInInput) { return; } // Prevent default behavior (e.g., typing '/' into another focused input or browser's quick find) event.preventDefault(); // Focus the target input (using the original ref.value which is fine for DOM methods) targetInputRef.value.focus(); } }; onMounted(() => { window.addEventListener('keydown', handleGlobalKeyPress); }); onBeforeUnmount(() => { window.removeEventListener('keydown', handleGlobalKeyPress); }); } ================================================ FILE: web/src/views/chat/hooks/useUsingContext.ts ================================================ import { ref } from 'vue' import { useMessage } from 'naive-ui' import { t } from '@/locales' export function useUsingContext() { const ms = useMessage() const usingContext = ref(true) function toggleUsingContext() { usingContext.value = !usingContext.value if (usingContext.value) ms.success(t('chat.turnOnContext')) else ms.warning(t('chat.turnOffContext')) } return { usingContext, toggleUsingContext, } } ================================================ FILE: web/src/views/chat/index.vue ================================================ ================================================ FILE: web/src/views/chat/layout/Layout.vue ================================================ ================================================ FILE: web/src/views/chat/layout/index.ts ================================================ import ChatLayout from './Layout.vue' export { ChatLayout } ================================================ FILE: web/src/views/chat/layout/sider/Footer.vue ================================================ ================================================ FILE: web/src/views/chat/layout/sider/List.vue ================================================ ================================================ FILE: web/src/views/chat/layout/sider/index.vue ================================================ ================================================ FILE: web/src/views/components/Avatar/MessageAvatar.vue ================================================ ================================================ FILE: web/src/views/components/Avatar/ModelAvatar.vue ================================================ ================================================ FILE: web/src/views/components/Message/AnswerContent.vue ================================================ ================================================ FILE: web/src/views/components/Message/Text.vue ================================================ ================================================ FILE: web/src/views/components/Message/ThinkingRenderer.vue ================================================ ================================================ FILE: web/src/views/components/Message/Util.ts ================================================ interface ThinkResult { thinkPart: string answerPart: string } function parseText(text: string): ThinkResult { let thinkContent = '' const answerContent = text.replace(/(.*?)<\/think>/gs, (match, content) => { thinkContent = content.trim() return '' }) return { thinkPart: thinkContent, answerPart: answerContent, } } export { parseText, ThinkResult } ================================================ FILE: web/src/views/components/Message/style.less ================================================ .markdown-body { background-color: transparent; font-size: 14px; p { white-space: pre-wrap; } ol { list-style-type: decimal; } ul { list-style-type: disc; } pre code, pre tt { line-height: 1.65; } .highlight pre, pre { background-color: #fff; } code.hljs { padding: 0; } .code-block { &-wrapper { position: relative; padding-top: 24px; } &-header { position: absolute; top: 5px; right: 0; width: 100%; padding: 0 1rem; display: flex; justify-content: flex-end; align-items: center; color: #b3b3b3; &__copy{ cursor: pointer; margin-left: 0.5rem; user-select: none; &:hover { color: #65a665; } } } } } html.dark { .highlight pre, pre { background-color: #282c34; } } @media screen and (max-width: 533px) { .markdown-body .code-block-wrapper { padding: unset; code { padding: 24px 16px 16px 16px; } } } /* Additional responsive improvements for better layout handling */ @media screen and (max-width: 639px) { .markdown-body { /* Ensure content doesn't break mobile layout */ max-width: 100%; overflow-x: hidden; word-wrap: break-word; overflow-wrap: break-word; } .markdown-body pre { /* Better code block handling on mobile */ max-width: 100%; overflow-x: auto; white-space: pre; word-wrap: normal; -webkit-overflow-scrolling: touch; } .markdown-body table { /* Ensure tables don't break layout on mobile */ max-width: 100%; overflow-x: auto; display: block; white-space: nowrap; } } ================================================ FILE: web/src/views/components/Message/thinkingParser.ts ================================================ import type { ThinkingParseResult, ThinkingCacheEntry, ThinkingParserConfig } from './types/thinking' class ThinkingParser { private cache: Map = new Map() private config: Required constructor(config: ThinkingParserConfig = {}) { this.config = { cacheSize: 100, cacheTTL: 5 * 60 * 1000, // 5 minutes enableLogging: false, thinkingTagPattern: /(.*?)<\/think>/gs, ...config } } parseText(text: string): ThinkingParseResult { // Check cache first const cacheKey = this.generateCacheKey(text) const cached = this.getFromCache(cacheKey) if (cached) { if (this.config.enableLogging) { console.log('Cache hit for thinking content') } return cached } // Parse thinking content const thinkingContents: string[] = [] let answerContent = text // Check for complete thinking tags first const completeMatch = text.match(this.config.thinkingTagPattern) if (completeMatch) { // Complete thinking tags found, extract content answerContent = text.replace(this.config.thinkingTagPattern, (_, content) => { thinkingContents.push(content.trim()) return '' }) } else { // Check for incomplete thinking tags (opening without closing) const openingTagMatch = text.match(//) const closingTagMatch = text.match(/<\/think>/) if (openingTagMatch && !closingTagMatch) { // Incomplete: has opening tag but no closing tag // Extract content after opening tag as thinking content const openingTagIndex = text.indexOf('') const content = text.substring(openingTagIndex + 7) // 7 is length of '' // Always add content, even if empty - this indicates we're in thinking mode thinkingContents.push(content) answerContent = text.substring(0, openingTagIndex) } else if (!openingTagMatch && closingTagMatch) { // Incomplete: has closing tag but no opening tag // Treat everything before closing tag as thinking content const closingTagIndex = text.indexOf('') const content = text.substring(0, closingTagIndex) // Always add content, even if empty - this indicates thinking content was present thinkingContents.push(content) answerContent = '' } // If both tags are missing or both are present (already handled), no special handling needed } const thinkingContentStr = thinkingContents.map(content => content.trim()).join('\n\n') // We have thinking if there's content OR if we found an incomplete opening tag const hasThinkingContent = thinkingContents.length > 0 const result: ThinkingParseResult = { hasThinking: hasThinkingContent, thinkingContent: { content: thinkingContentStr, isExpanded: true, createdAt: new Date(), updatedAt: new Date() }, answerContent, rawText: text } // Cache the result this.setToCache(cacheKey, result) if (this.config.enableLogging) { console.log('Parsed thinking content:', { hasThinking: result.hasThinking, thinkingLength: thinkingContentStr.length, answerLength: answerContent.length }) } return result } private generateCacheKey(text: string): string { // Simple hash function for cache key let hash = 0 for (let i = 0; i < text.length; i++) { const char = text.charCodeAt(i) hash = ((hash << 5) - hash) + char hash = hash & hash // Convert to 32-bit integer } return hash.toString(36) } private getFromCache(key: string): ThinkingParseResult | null { const entry = this.cache.get(key) if (!entry) return null // Check TTL const now = Date.now() if (now - entry.timestamp > this.config.cacheTTL) { this.cache.delete(key) return null } return entry.parsedResult } private setToCache(key: string, result: ThinkingParseResult): void { // Clean up cache if it's too large if (this.cache.size >= this.config.cacheSize) { this.cleanupCache() } this.cache.set(key, { rawText: result.rawText, parsedResult: result, timestamp: Date.now() }) } private cleanupCache(): void { // Remove oldest entries const entries = Array.from(this.cache.entries()) const toRemove = entries.slice(0, Math.floor(this.config.cacheSize * 0.3)) toRemove.forEach(([key]) => { this.cache.delete(key) }) if (this.config.enableLogging) { console.log(`Cleaned up ${toRemove.length} cache entries`) } } clearCache(): void { this.cache.clear() if (this.config.enableLogging) { console.log('Thinking parser cache cleared') } } getCacheStats(): { size: number; hitRate: number } { return { size: this.cache.size, hitRate: 0 // Could be enhanced with hit tracking } } } // Export singleton instance export const thinkingParser = new ThinkingParser() // Export utility functions export const parseThinkingContent = (text: string): ThinkingParseResult => { return thinkingParser.parseText(text) } export const clearThinkingCache = (): void => { thinkingParser.clearCache() } export const getThinkingCacheStats = () => { return thinkingParser.getCacheStats() } // Backward compatibility - re-export types export type { ThinkingParseResult, ThinkingCacheEntry, ThinkingParserConfig } from './types/thinking' ================================================ FILE: web/src/views/components/Message/types/thinking.ts ================================================ export interface ThinkingContent { id?: string content: string isExpanded?: boolean createdAt?: Date updatedAt?: Date } export interface ThinkingParseResult { hasThinking: boolean thinkingContent: ThinkingContent answerContent: string rawText: string } export interface ThinkingRenderOptions { enableMarkdown?: boolean enableCollapsible?: boolean defaultExpanded?: boolean showBorder?: boolean borderColor?: string maxLines?: number enableCopy?: boolean } export interface ThinkingCacheEntry { rawText: string parsedResult: ThinkingParseResult timestamp: number } export interface ThinkingParserConfig { cacheSize?: number cacheTTL?: number enableLogging?: boolean thinkingTagPattern?: RegExp } export interface ThinkingComposableReturn { thinkingContent: ThinkingContent | null hasThinking: boolean isExpanded: boolean toggleExpanded: () => void setExpanded: (expanded: boolean) => void parsedResult: ThinkingParseResult | null refreshParse: () => void updateText: (newText: string) => void } export interface ThinkingRendererProps { content: ThinkingContent options?: ThinkingRenderOptions class?: string onToggle?: (expanded: boolean) => void } export interface ThinkingRendererEmits { toggle: [expanded: boolean] copy: [content: string] } export interface UseThinkingContentOptions { defaultExpanded?: boolean enableCache?: boolean parserConfig?: ThinkingParserConfig } ================================================ FILE: web/src/views/components/Message/useThinkingContent.ts ================================================ import { ref, computed, watch } from 'vue' import { parseThinkingContent } from './thinkingParser' import type { ThinkingContent, ThinkingParseResult, ThinkingComposableReturn, UseThinkingContentOptions } from './types/thinking' export function useThinkingContent( text: string | undefined, options: UseThinkingContentOptions = {} ): ThinkingComposableReturn { const { defaultExpanded = true, enableCache = true, parserConfig = {} } = options const isExpanded = ref(defaultExpanded) const rawText = ref(text || '') const parsedResult = ref(null) // Parse content when raw text changes const parseContent = () => { if (!rawText.value) { parsedResult.value = { hasThinking: false, thinkingContent: { content: '', isExpanded: defaultExpanded }, answerContent: '', rawText: '' } return } parsedResult.value = parseThinkingContent(rawText.value) } // Initial parse parseContent() // Watch for text changes watch(rawText, parseContent, { immediate: true }) // Computed properties const thinkingContent = computed(() => parsedResult.value?.thinkingContent || null) const hasThinking = computed(() => parsedResult.value?.hasThinking || false) // Methods const toggleExpanded = () => { isExpanded.value = !isExpanded.value if (thinkingContent.value) { thinkingContent.value.isExpanded = isExpanded.value } } const setExpanded = (expanded: boolean) => { isExpanded.value = expanded if (thinkingContent.value) { thinkingContent.value.isExpanded = expanded } } const refreshParse = () => { parseContent() } const updateText = (newText: string) => { rawText.value = newText } return { thinkingContent, hasThinking, isExpanded, toggleExpanded, setExpanded, parsedResult, refreshParse, updateText } } // Composable for managing multiple thinking contents export function useMultipleThinkingContent( texts: Array<{ id: string; text: string }>, options: UseThinkingContentOptions = {} ) { const thinkingStates = ref(new Map()) const getThinkingState = (id: string, text: string) => { if (!thinkingStates.value.has(id)) { thinkingStates.value.set(id, useThinkingContent(text, options)) } return thinkingStates.value.get(id)! } const updateText = (id: string, newText: string) => { const state = thinkingStates.value.get(id) if (state) { state.updateText(newText) } } const removeThinkingState = (id: string) => { thinkingStates.value.delete(id) } const clearAllStates = () => { thinkingStates.value.clear() } return { getThinkingState, updateText, removeThinkingState, clearAllStates, states: thinkingStates } } // Composable for thinking content statistics export function useThinkingStats() { const totalParsed = ref(0) const totalWithThinking = ref(0) const averageThinkingLength = ref(0) const updateStats = (parsedResult: ThinkingParseResult) => { totalParsed.value++ if (parsedResult.hasThinking) { totalWithThinking.value++ const thinkingLength = parsedResult.thinkingContent.content.length averageThinkingLength.value = (averageThinkingLength.value * (totalWithThinking.value - 1) + thinkingLength) / totalWithThinking.value } } const getStats = computed(() => ({ totalParsed: totalParsed.value, totalWithThinking: totalWithThinking.value, thinkingRate: totalParsed.value > 0 ? (totalWithThinking.value / totalParsed.value) * 100 : 0, averageThinkingLength: Math.round(averageThinkingLength.value) })) const resetStats = () => { totalParsed.value = 0 totalWithThinking.value = 0 averageThinkingLength.value = 0 } return { updateStats, getStats, resetStats } } ================================================ FILE: web/src/views/components/Permission.vue ================================================ ================================================ FILE: web/src/views/exception/404/index.vue ================================================ ================================================ FILE: web/src/views/exception/500/index.vue ================================================ ================================================ FILE: web/src/views/prompt/components/Definitions.vue ================================================ ================================================ FILE: web/src/views/prompt/components/PromptCreator.vue ================================================ ================================================ FILE: web/src/views/prompt/components/PromptProcess.vue ================================================ ================================================ FILE: web/src/views/prompt/creator.vue ================================================ ================================================ FILE: web/src/views/snapshot/all.vue ================================================ ================================================ FILE: web/src/views/snapshot/components/Comment/index.vue ================================================ ================================================ FILE: web/src/views/snapshot/components/Header/index.vue ================================================ ================================================ FILE: web/src/views/snapshot/components/Message/index.vue ================================================ ================================================ FILE: web/src/views/snapshot/components/Message/style.less ================================================ .markdown-body { background-color: transparent; font-size: 14px; p { white-space: pre-wrap; } ol { list-style-type: decimal; } ul { list-style-type: disc; } pre code, pre tt { line-height: 1.65; } .highlight pre, pre { background-color: #fff; } code.hljs { padding: 0; } .code-block { &-wrapper { position: relative; padding-top: 24px; } &-header { position: absolute; top: 5px; right: 0; width: 100%; padding: 0 1rem; display: flex; justify-content: flex-end; align-items: center; color: #b3b3b3; &__copy{ cursor: pointer; margin-left: 0.5rem; user-select: none; &:hover { color: #65a665; } } } } } html.dark { .highlight pre, pre { background-color: #282c34; } } ================================================ FILE: web/src/views/snapshot/components/Search.vue ================================================ ================================================ FILE: web/src/views/snapshot/page.vue ================================================ ================================================ FILE: web/tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ module.exports = { darkMode: 'class', content: [ './index.html', './src/**/*.{vue,js,ts,jsx,tsx}', ], theme: { extend: { animation: { blink: 'blink 1.2s infinite steps(1, start)', }, keyframes: { blink: { '0%, 100%': { 'background-color': 'currentColor' }, '50%': { 'background-color': 'transparent' }, }, }, }, }, plugins: [], } ================================================ FILE: web/tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "module": "ESNext", "target": "ESNext", "lib": ["DOM", "ESNext"], "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "jsx": "preserve", "moduleResolution": "node", "resolveJsonModule": true, "noUnusedLocals": true, "strictNullChecks": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "paths": { "@/*": ["./src/*"] }, "types": ["vite/client", "node", "naive-ui/volar"] }, "exclude": ["node_modules", "dist", "service"] }