diff --git a/cypress/e2e/chat/model-selection-persistence.cy.ts b/cypress/e2e/chat/model-selection-persistence.cy.ts
new file mode 100644
index 00000000..afaf9e6b
--- /dev/null
+++ b/cypress/e2e/chat/model-selection-persistence.cy.ts
@@ -0,0 +1,205 @@
+import { v4 as uuidv4 } from 'uuid';
+import { AuthTestUtils } from '../../support/auth-utils';
+import { TestTool } from '../../support/page-utils';
+import { PageSelectors, SidebarSelectors, ModelSelectorSelectors } from '../../support/selectors';
+
+describe('Chat Model Selection Persistence Tests', () => {
+ const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL');
+ const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL');
+ const generateRandomEmail = () => `${uuidv4()}@appflowy.io`;
+ let testEmail: string;
+
+ before(() => {
+ // Log environment configuration for debugging
+ cy.task('log', `Test Environment Configuration:
+ - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL}
+ - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`);
+ });
+
+ beforeEach(() => {
+ // Generate unique test data for each test
+ testEmail = generateRandomEmail();
+ });
+
+ describe('Model Selection Persistence', () => {
+ it('should persist selected model after page reload', () => {
+ // Handle uncaught exceptions during workspace creation
+ cy.on('uncaught:exception', (err: Error) => {
+ if (err.message.includes('No workspace or service found')) {
+ return false;
+ }
+ if (err.message.includes('View not found')) {
+ return false;
+ }
+ if (err.message.includes('WebSocket') || err.message.includes('connection')) {
+ return false;
+ }
+ return true;
+ });
+
+ // Step 1: Login
+ cy.task('log', '=== Step 1: Login ===');
+ cy.visit('/login', { failOnStatusCode: false });
+ cy.wait(2000);
+
+ const authUtils = new AuthTestUtils();
+ authUtils.signInWithTestUrl(testEmail).then(() => {
+ cy.url().should('include', '/app');
+
+ // Wait for the app to fully load
+ cy.task('log', 'Waiting for app to fully load...');
+
+ // Wait for the loading screen to disappear and main app to appear
+ cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!');
+
+ // Wait for the sidebar to be visible (indicates app is loaded)
+ SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 });
+
+ // Wait for at least one page to exist in the sidebar
+ PageSelectors.names().should('exist', { timeout: 30000 });
+
+ // Additional wait for stability
+ cy.wait(2000);
+
+ // Step 2: Create an AI Chat
+ cy.task('log', '=== Step 2: Creating AI Chat ===');
+
+ // Expand the first space to see its pages
+ TestTool.expandSpace();
+ cy.wait(1000);
+
+ // Find the first page item and hover over it to show actions
+ PageSelectors.items().first().then($page => {
+ cy.task('log', 'Hovering over first page to show action buttons...');
+
+ // Hover over the page to reveal the action buttons
+ cy.wrap($page)
+ .trigger('mouseenter', { force: true })
+ .trigger('mouseover', { force: true });
+
+ cy.wait(1000);
+
+ // Click the inline add button (plus icon)
+ cy.wrap($page).within(() => {
+ cy.get('[data-testid="inline-add-page"]')
+ .first()
+ .should('be.visible')
+ .click({ force: true });
+ });
+ });
+
+ // Wait for the dropdown menu to appear
+ cy.wait(1000);
+
+ // Click on the AI Chat option from the dropdown
+ cy.get('[data-testid="add-ai-chat-button"]')
+ .should('be.visible')
+ .click();
+
+ cy.task('log', 'Created AI Chat');
+
+ // Wait for navigation to the AI chat page
+ cy.wait(3000);
+
+ // Step 3: Open model selector and select a model
+ cy.task('log', '=== Step 3: Selecting a Model ===');
+
+ // Wait for the chat interface to load
+ cy.wait(2000);
+
+ // Click on the model selector button
+ ModelSelectorSelectors.button()
+ .should('be.visible', { timeout: 10000 })
+ .click();
+
+ cy.task('log', 'Opened model selector dropdown');
+
+ // Wait for the dropdown to appear and models to load
+ cy.wait(2000);
+
+ // Select a specific model (we'll select the first non-Auto model if available)
+ ModelSelectorSelectors.options()
+ .then($options => {
+ // Find a model that's not "Auto"
+ const nonAutoOptions = $options.filter((i, el) => {
+ const testId = el.getAttribute('data-testid');
+ return testId && !testId.includes('model-option-Auto');
+ });
+
+ if (nonAutoOptions.length > 0) {
+ // Click the first non-Auto model
+ const selectedModel = nonAutoOptions[0].getAttribute('data-testid')?.replace('model-option-', '');
+ cy.task('log', `Selecting model: ${selectedModel}`);
+ cy.wrap(nonAutoOptions[0]).click();
+
+ // Store the selected model name for verification
+ cy.wrap(selectedModel).as('selectedModel');
+ } else {
+ // If only Auto is available, select it explicitly
+ cy.task('log', 'Only Auto model available, selecting it');
+ ModelSelectorSelectors.optionByName('Auto').click();
+ cy.wrap('Auto').as('selectedModel');
+ }
+ });
+
+ // Wait for the selection to be applied
+ cy.wait(1000);
+
+ // Verify the model is selected by checking the button text
+ cy.get('@selectedModel').then((modelName) => {
+ cy.task('log', `Verifying model ${modelName} is displayed in button`);
+ ModelSelectorSelectors.button()
+ .should('contain.text', modelName);
+ });
+
+ // Step 4: Save the current URL for reload
+ cy.task('log', '=== Step 4: Saving current URL ===');
+ cy.url().then(url => {
+ cy.wrap(url).as('chatUrl');
+ cy.task('log', `Current chat URL: ${url}`);
+ });
+
+ // Step 5: Reload the page
+ cy.task('log', '=== Step 5: Reloading page ===');
+ cy.reload();
+
+ // Wait for the page to reload completely
+ cy.wait(3000);
+
+ // Step 6: Verify the model selection persisted
+ cy.task('log', '=== Step 6: Verifying Model Selection Persisted ===');
+
+ // Wait for the model selector button to be visible again
+ ModelSelectorSelectors.button()
+ .should('be.visible', { timeout: 10000 });
+
+ // Verify the previously selected model is still displayed
+ cy.get('@selectedModel').then((modelName) => {
+ cy.task('log', `Checking if model ${modelName} is still selected after reload`);
+ ModelSelectorSelectors.button()
+ .should('contain.text', modelName);
+ cy.task('log', `✓ Model ${modelName} persisted after page reload!`);
+ });
+
+ // Optional: Open the dropdown again to verify the selection visually
+ cy.task('log', '=== Step 7: Double-checking selection in dropdown ===');
+ ModelSelectorSelectors.button().click();
+ cy.wait(1000);
+
+ // Verify the selected model has the selected styling
+ cy.get('@selectedModel').then((modelName) => {
+ ModelSelectorSelectors.optionByName(modelName as string)
+ .should('have.class', 'bg-fill-content-select');
+ cy.task('log', `✓ Model ${modelName} shows as selected in dropdown`);
+ });
+
+ // Close the dropdown
+ cy.get('body').click(0, 0);
+
+ // Final verification
+ cy.task('log', '=== Test completed successfully! ===');
+ cy.task('log', '✓✓✓ Model selection persisted after page reload');
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/cypress/support/selectors.ts b/cypress/support/selectors.ts
index 6eada2d3..a5d5e089 100644
--- a/cypress/support/selectors.ts
+++ b/cypress/support/selectors.ts
@@ -184,6 +184,27 @@ export const SidebarSelectors = {
pageHeader: () => cy.get(byTestId('sidebar-page-header')),
};
+/**
+ * Chat Model Selector-related selectors
+ * Used for testing AI model selection in chat interface
+ */
+export const ModelSelectorSelectors = {
+ // Model selector button
+ button: () => cy.get(byTestId('model-selector-button')),
+
+ // Model search input
+ searchInput: () => cy.get(byTestId('model-search-input')),
+
+ // Get all model options
+ options: () => cy.get('[data-testid^="model-option-"]'),
+
+ // Get specific model option by name
+ optionByName: (modelName: string) => cy.get(byTestId(`model-option-${modelName}`)),
+
+ // Get selected model option (has the selected class)
+ selectedOption: () => cy.get('[data-testid^="model-option-"]').filter('.bg-fill-content-select'),
+};
+
/**
* Database Grid-related selectors
*/
diff --git a/src/components/chat/chat/main.tsx b/src/components/chat/chat/main.tsx
index efc131d7..2b34a7ba 100644
--- a/src/components/chat/chat/main.tsx
+++ b/src/components/chat/chat/main.tsx
@@ -1,47 +1,72 @@
// Code: Chat main component
-import { ChatContext } from './context';
import { ChatInput } from '@/components/chat/components/chat-input';
import { ChatMessages } from '@/components/chat/components/chat-messages';
+import { ModelSelectorContext } from '@/components/chat/contexts/model-selector-context';
import { cn } from '@/components/chat/lib/utils';
-import { MessageAnimationProvider } from '@/components/chat/provider/message-animation-provider';
import { EditorProvider } from '@/components/chat/provider/editor-provider';
-import { MessagesHandlerProvider } from '@/components/chat/provider/messages-handler-provider';
+import { MessageAnimationProvider } from '@/components/chat/provider/message-animation-provider';
+import { MessagesHandlerProvider, useMessagesHandlerContext } from '@/components/chat/provider/messages-handler-provider';
import { ChatMessagesProvider } from '@/components/chat/provider/messages-provider';
import { PromptModalProvider } from '@/components/chat/provider/prompt-modal-provider';
import { ResponseFormatProvider } from '@/components/chat/provider/response-format-provider';
import { SelectionModeProvider } from '@/components/chat/provider/selection-mode-provider';
import { SuggestionsProvider } from '@/components/chat/provider/suggestions-provider';
-import { ChatProps } from '@/components/chat/types';
-import { AnimatePresence, motion } from 'framer-motion';
import { ViewLoaderProvider } from '@/components/chat/provider/view-loader-provider';
-import { ModelSelectorContext } from '@/components/chat/contexts/model-selector-context';
+import { ChatProps, User } from '@/components/chat/types';
+import { AnimatePresence, motion } from 'framer-motion';
+import { ChatContext, useChatContext } from './context';
+
+// Component to bridge ModelSelector with MessagesHandler
+function ChatContentWithModelSync({ currentUser, selectionMode }: { currentUser?: User; selectionMode?: boolean }) {
+ const { selectedModelName, setSelectedModelName } = useMessagesHandlerContext();
+ const { requestInstance, chatId } = useChatContext();
+
+ return (
+