feat: Improve UI layout and navigation

- Increase logo size (48x48 desktop, 56x56 mobile) for better visibility
- Add logo as favicon
- Add logo to mobile header
- Move user menu to navigation bars (sidebar on desktop, bottom bar on mobile)
- Fix desktop chat layout - container structure prevents voice controls cutoff
- Fix mobile bottom bar - use icon-only ActionIcons instead of truncated text buttons
- Hide Create Node/New Conversation buttons on mobile to save header space
- Make fixed header and voice controls work properly with containers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-09 14:43:11 +00:00
parent 47b35b9caf
commit 0ed2d6c0b3
57 changed files with 6996 additions and 629 deletions

143
tests/voice-mode.spec.ts Normal file
View File

@@ -0,0 +1,143 @@
import { test, expect } from '@playwright/test';
test.describe('Voice Mode', () => {
test.beforeEach(async ({ page }) => {
// Navigate to chat page (should be authenticated via setup)
await page.goto('/chat');
await expect(page.getByText('Ponderants Interview')).toBeVisible();
});
test('should start voice conversation and display correct button text', async ({ page }) => {
// Initial state - button should show "Start Voice Conversation"
const voiceButton = page.getByRole('button', { name: /Start Voice Conversation/i });
await expect(voiceButton).toBeVisible();
// Click to start voice mode
await voiceButton.click();
// Button should transition to one of the active states
// Could be "Generating speech..." if there's a greeting, or "Listening..." if no greeting
await expect(page.getByRole('button', { name: /Generating speech|Listening|Checking for greeting/i })).toBeVisible({
timeout: 5000,
});
});
test('should skip audio during generation and transition to listening', async ({ page }) => {
// Start voice mode
const voiceButton = page.getByRole('button', { name: /Start Voice Conversation/i });
await voiceButton.click();
// Wait for generation or playing state
await expect(page.getByRole('button', { name: /Generating speech|AI is speaking/i })).toBeVisible({
timeout: 5000,
});
// Skip button should be visible
const skipButton = page.getByRole('button', { name: /Skip/i });
await expect(skipButton).toBeVisible();
// Click skip
await skipButton.click();
// Should transition to listening state
await expect(page.getByRole('button', { name: /Listening/i })).toBeVisible({ timeout: 3000 });
});
test('should use test buttons to simulate full conversation flow', async ({ page }) => {
// Start voice mode
await page.getByRole('button', { name: /Start Voice Conversation/i }).click();
// Wait for initial state (could be checking, generating, or listening)
await page.waitForTimeout(1000);
// If there's a skip button (greeting is playing), click it
const skipButton = page.getByRole('button', { name: /Skip/i });
if (await skipButton.isVisible()) {
await skipButton.click();
}
// Should eventually reach listening state
await expect(page.getByRole('button', { name: /Listening/i })).toBeVisible({ timeout: 5000 });
// In development mode, test buttons should be visible
const isDevelopment = process.env.NODE_ENV !== 'production';
if (isDevelopment) {
// Click "Simulate User Speech" test button
const simulateSpeechButton = page.getByRole('button', { name: /Simulate Speech/i });
await expect(simulateSpeechButton).toBeVisible();
await simulateSpeechButton.click();
// Should transition to userSpeaking state
await expect(page.getByRole('button', { name: /Speaking/i })).toBeVisible({ timeout: 2000 });
// Add a phrase using test button
const addPhraseButton = page.getByRole('button', { name: /Add Phrase/i });
await addPhraseButton.click();
// Should be in timingOut state
await expect(page.getByRole('button', { name: /auto-submit/i })).toBeVisible({ timeout: 2000 });
// Trigger timeout using test button
const triggerTimeoutButton = page.getByRole('button', { name: /Trigger Timeout/i });
await triggerTimeoutButton.click();
// Should submit and wait for AI
await expect(page.getByRole('button', { name: /Submitting|Waiting for AI/i })).toBeVisible({
timeout: 2000,
});
// Wait for AI response (this will take a few seconds)
await expect(page.getByRole('button', { name: /Generating speech|AI is speaking/i })).toBeVisible({
timeout: 15000,
});
// Skip the AI audio
const skipAudioButton = page.getByRole('button', { name: /Skip/i });
if (await skipAudioButton.isVisible()) {
await skipAudioButton.click();
}
// Should return to listening
await expect(page.getByRole('button', { name: /Listening/i })).toBeVisible({ timeout: 3000 });
}
});
test('should stop voice mode and return to idle', async ({ page }) => {
// Start voice mode
const voiceButton = page.getByRole('button', { name: /Start Voice Conversation/i });
await voiceButton.click();
// Wait for active state
await page.waitForTimeout(1000);
// Click the button again to stop
await page.getByRole('button', { name: /Listening|Speaking|Generating|AI is speaking/i }).click();
// Should return to idle state
await expect(page.getByRole('button', { name: /Start Voice Conversation/i })).toBeVisible({
timeout: 2000,
});
});
test('should disable text input while voice mode is active', async ({ page }) => {
const textInput = page.getByPlaceholder(/type your thoughts here/i);
// Text input should be enabled initially
await expect(textInput).toBeEnabled();
// Start voice mode
await page.getByRole('button', { name: /Start Voice Conversation/i }).click();
// Wait for voice mode to activate
await page.waitForTimeout(1000);
// Text input should be disabled
await expect(textInput).toBeDisabled();
// Stop voice mode
await page.getByRole('button', { name: /Listening|Speaking|Generating|AI is speaking/i }).click();
// Text input should be enabled again
await expect(textInput).toBeEnabled();
});
});