From b8b5acc1f0b3d3768eb7658821df633d5e3d0287 Mon Sep 17 00:00:00 2001 From: Kalo Chin Date: Wed, 9 Jul 2025 01:14:03 +0900 Subject: [PATCH] Add always_new_chat option to apps and sites Introduces a new boolean field 'always_new_chat' (default true) to both apps and sites in the backend and frontend. This option allows configuration to always start a new chat session, ignoring previous conversations. Includes database migration, API/controller updates, model changes, UI controls, and i18n support. --- api/controllers/console/app/app.py | 1 + api/controllers/console/app/site.py | 2 ++ api/controllers/web/site.py | 1 + api/fields/app_fields.py | 5 ++++ .../2025_07_08_0001_add_always_new_chat.py | 24 +++++++++++++++++++ api/models/model.py | 2 ++ api/services/app_service.py | 1 + .../app/overview/settings/index.tsx | 20 +++++++++++++++- .../base/chat/chat-with-history/hooks.tsx | 8 ++++++- .../base/chat/embedded-chatbot/hooks.tsx | 9 +++++-- web/i18n/en-US/app.ts | 4 ++++ web/models/share.ts | 1 + 12 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 api/migrations/versions/2025_07_08_0001_add_always_new_chat.py diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 860166a61a..5c5c8167bc 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -151,6 +151,7 @@ class AppApi(Resource): parser.add_argument("icon", type=str, location="json") parser.add_argument("icon_background", type=str, location="json") parser.add_argument("use_icon_as_answer_icon", type=bool, location="json") + parser.add_argument("always_new_chat", type=bool, location="json") args = parser.parse_args() app_service = AppService() diff --git a/api/controllers/console/app/site.py b/api/controllers/console/app/site.py index 3c3a359eeb..065f931245 100644 --- a/api/controllers/console/app/site.py +++ b/api/controllers/console/app/site.py @@ -34,6 +34,7 @@ def parse_app_site_args(): parser.add_argument("prompt_public", type=bool, required=False, location="json") parser.add_argument("show_workflow_steps", type=bool, required=False, location="json") parser.add_argument("use_icon_as_answer_icon", type=bool, required=False, location="json") + parser.add_argument("always_new_chat", type=bool, required=False, location="json") return parser.parse_args() @@ -71,6 +72,7 @@ class AppSite(Resource): "prompt_public", "show_workflow_steps", "use_icon_as_answer_icon", + "always_new_chat", ]: value = args.get(attr_name) if value is not None: diff --git a/api/controllers/web/site.py b/api/controllers/web/site.py index 0564b15ea3..a2ff3d09b4 100644 --- a/api/controllers/web/site.py +++ b/api/controllers/web/site.py @@ -40,6 +40,7 @@ class AppSiteApi(WebApiResource): "prompt_public": fields.Boolean, "show_workflow_steps": fields.Boolean, "use_icon_as_answer_icon": fields.Boolean, + "always_new_chat": fields.Boolean, } app_fields = { diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index 500ca47c7e..f9e85cf068 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -59,6 +59,7 @@ app_detail_fields = { "workflow": fields.Nested(workflow_partial_fields, allow_null=True), "tracing": fields.Raw, "use_icon_as_answer_icon": fields.Boolean, + "always_new_chat": fields.Boolean, "created_by": fields.String, "created_at": TimestampField, "updated_by": fields.String, @@ -94,6 +95,7 @@ app_partial_fields = { "model_config": fields.Nested(model_config_partial_fields, attribute="app_model_config", allow_null=True), "workflow": fields.Nested(workflow_partial_fields, allow_null=True), "use_icon_as_answer_icon": fields.Boolean, + "always_new_chat": fields.Boolean, "created_by": fields.String, "created_at": TimestampField, "updated_by": fields.String, @@ -147,6 +149,7 @@ site_fields = { "app_base_url": fields.String, "show_workflow_steps": fields.Boolean, "use_icon_as_answer_icon": fields.Boolean, + "always_new_chat": fields.Boolean, "created_by": fields.String, "created_at": TimestampField, "updated_by": fields.String, @@ -175,6 +178,7 @@ app_detail_fields_with_site = { "site": fields.Nested(site_fields), "api_base_url": fields.String, "use_icon_as_answer_icon": fields.Boolean, + "always_new_chat": fields.Boolean, "created_by": fields.String, "created_at": TimestampField, "updated_by": fields.String, @@ -201,6 +205,7 @@ app_site_fields = { "prompt_public": fields.Boolean, "show_workflow_steps": fields.Boolean, "use_icon_as_answer_icon": fields.Boolean, + "always_new_chat": fields.Boolean, } leaked_dependency_fields = {"type": fields.String, "value": fields.Raw, "current_identifier": fields.String} diff --git a/api/migrations/versions/2025_07_08_0001_add_always_new_chat.py b/api/migrations/versions/2025_07_08_0001_add_always_new_chat.py new file mode 100644 index 0000000000..6a2c136cc6 --- /dev/null +++ b/api/migrations/versions/2025_07_08_0001_add_always_new_chat.py @@ -0,0 +1,24 @@ +"""add always_new_chat boolean field to apps and sites (default true) + +Revision ID: 0001_add_always_new_chat +Revises: +Create Date: 2025-07-08 00:00:00.000000 +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'd57ba9ebb254' +down_revision = '0ab65e1cc7fa' +branch_labels = None +depends_on = None + +def upgrade(): + for table in ('apps', 'sites'): + op.add_column(table, sa.Column('always_new_chat', sa.Boolean(), nullable=False, server_default=sa.text('true'))) + + +def downgrade(): + for table in ('apps', 'sites'): + op.drop_column(table, 'always_new_chat') \ No newline at end of file diff --git a/api/models/model.py b/api/models/model.py index 93737043d5..85c66222a0 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -100,6 +100,7 @@ class App(Base): updated_by = db.Column(StringUUID, nullable=True) updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) + always_new_chat = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) @property def desc_or_prompt(self): @@ -1478,6 +1479,7 @@ class Site(Base): privacy_policy = db.Column(db.String(255)) show_workflow_steps = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) + always_new_chat = db.Column(db.Boolean, nullable=False, server_default=db.text("true")) _custom_disclaimer: Mapped[str] = mapped_column("custom_disclaimer", sa.TEXT, default="") customize_domain = db.Column(db.String(255)) customize_token_strategy = db.Column(db.String(255), nullable=False) diff --git a/api/services/app_service.py b/api/services/app_service.py index d08462d001..bd66033015 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -235,6 +235,7 @@ class AppService: app.icon = args.get("icon") app.icon_background = args.get("icon_background") app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False) + app.always_new_chat = args.get("always_new_chat", True) app.updated_by = current_user.id app.updated_at = datetime.now(UTC).replace(tzinfo=None) db.session.commit() diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 524c340a53..6dd51b4fdc 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -51,6 +51,7 @@ export type ConfigParams = { icon_background?: string show_workflow_steps: boolean use_icon_as_answer_icon: boolean + always_new_chat: boolean enable_sso?: boolean } @@ -80,6 +81,7 @@ const SettingsModal: FC = ({ default_language, show_workflow_steps, use_icon_as_answer_icon, + always_new_chat, } = appInfo.site const [inputInfo, setInputInfo] = useState({ title, @@ -92,6 +94,7 @@ const SettingsModal: FC = ({ customDisclaimer: custom_disclaimer, show_workflow_steps, use_icon_as_answer_icon, + always_new_chat, enable_sso: appInfo.enable_sso, }) const [language, setLanguage] = useState(default_language) @@ -128,13 +131,14 @@ const SettingsModal: FC = ({ customDisclaimer: custom_disclaimer, show_workflow_steps, use_icon_as_answer_icon, + always_new_chat, enable_sso: appInfo.enable_sso, }) setLanguage(default_language) setAppIcon(icon_type === 'image' ? { type: 'image', url: icon_url!, fileId: icon } : { type: 'emoji', icon, background: icon_background! }) - }, [appInfo, chat_color_theme, chat_color_theme_inverted, copyright, custom_disclaimer, default_language, description, icon, icon_background, icon_type, icon_url, privacy_policy, show_workflow_steps, title, use_icon_as_answer_icon]) + }, [appInfo, chat_color_theme, chat_color_theme_inverted, copyright, custom_disclaimer, default_language, description, icon, icon_background, icon_type, icon_url, privacy_policy, show_workflow_steps, title, use_icon_as_answer_icon, always_new_chat]) const onHide = () => { onClose() @@ -196,6 +200,7 @@ const SettingsModal: FC = ({ icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, show_workflow_steps: inputInfo.show_workflow_steps, use_icon_as_answer_icon: inputInfo.use_icon_as_answer_icon, + always_new_chat: inputInfo.always_new_chat, enable_sso: inputInfo.enable_sso, } await onSave?.(params) @@ -291,6 +296,19 @@ const SettingsModal: FC = ({

{t('app.answerIcon.description')}

)} + {/* always new chat */} + {isChat && ( +
+
+
{t('app.alwaysNewChat.title')}
+ setInputInfo({ ...inputInfo, always_new_chat: v })} + /> +
+

{t('app.alwaysNewChat.description')}

+
+ )} {/* language */}
{t(`${prefixSettings}.language`)}
diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 32f74e6457..0dc169fda7 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -142,7 +142,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState>>(CONVERSATION_ID_INFO, { defaultValue: {}, }) - const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || '', [appId, conversationIdInfo, userId]) + const [userSelectedConversation, setUserSelectedConversation] = useState(false) + const currentConversationId = useMemo(() => { + if (appData?.site?.always_new_chat && !userSelectedConversation) + return '' + return conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || '' + }, [appId, conversationIdInfo, userId, appData, userSelectedConversation]) const handleConversationIdInfoChange = useCallback((changeConversationId: string) => { if (appId) { let prevValue = conversationIdInfo?.[appId || ''] @@ -373,6 +378,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [setShowNewConversationItemInList, checkInputsRequired]) const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: noop }) const handleChangeConversation = useCallback((conversationId: string) => { + setUserSelectedConversation(!!conversationId) currentChatInstanceRef.current.handleStop() setNewConversationId('') handleConversationIdInfoChange(conversationId) diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 7dd665efdc..8bafefbd10 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -120,8 +120,11 @@ export const useEmbeddedChatbot = () => { defaultValue: {}, }) const allowResetChat = !conversationId - const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '', - [appId, conversationIdInfo, userId, conversationId]) + const currentConversationId = useMemo(() => { + if (appData?.site?.always_new_chat && !userSelectedConversation) + return '' + return conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '' + }, [appId, conversationIdInfo, userId, conversationId, appData, userSelectedConversation]) const handleConversationIdInfoChange = useCallback((changeConversationId: string) => { if (appId) { let prevValue = conversationIdInfo?.[appId || ''] @@ -161,6 +164,7 @@ export const useEmbeddedChatbot = () => { ) const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false) + const [userSelectedConversation, setUserSelectedConversation] = useState(false) const pinnedConversationList = useMemo(() => { return appPinnedConversationData?.data || [] @@ -354,6 +358,7 @@ export const useEmbeddedChatbot = () => { }, [setShowNewConversationItemInList, checkInputsRequired]) const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: noop }) const handleChangeConversation = useCallback((conversationId: string) => { + setUserSelectedConversation(!!conversationId) currentChatInstanceRef.current.handleStop() setNewConversationId('') handleConversationIdInfoChange(conversationId) diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index c6f35d3df2..287e7d3334 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -117,6 +117,10 @@ const translation = { description: 'Whether to use the web app icon to replace 🤖 in the shared application', descriptionInExplore: 'Whether to use the web app icon to replace 🤖 in Explore', }, + alwaysNewChat: { + title: 'Always start a new chat', + description: 'Ignore previous conversation and open a new one when users visit the web app', + }, switch: 'Switch to Workflow Orchestrate', switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ', switchTip: 'not allow', diff --git a/web/models/share.ts b/web/models/share.ts index 3521365e82..6ca4a9998f 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -26,6 +26,7 @@ export type SiteInfo = { custom_disclaimer?: string show_workflow_steps?: boolean use_icon_as_answer_icon?: boolean + always_new_chat?: boolean } export type AppMeta = {