Files
doc_ai_frontend/docs/superpowers/plans/2026-03-27-workspace-improvements.md

29 KiB
Raw Blame History

Workspace Improvements Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Polish the workspace by removing logo/login button clutter, adding a history toggle, enforcing mandatory login after 3 guest uploads, and adding email verification code to the registration flow.

Architecture: Five independent changes across the workspace layer. History toggle state lives in WorkspacePage and is passed into LeftSidebar. Email verification adds a two-step flow inside AuthModal only — no new files needed. All i18n strings go through translations.ts.

Tech Stack: React 18, TypeScript, Tailwind CSS (utility classes), Lucide React icons, existing http client in lib/api.ts


File Map

File Change
src/components/layout/AppNavbar.tsx Remove logo icon + text + divider
src/components/LeftSidebar.tsx Remove login button; add history toggle prop
src/pages/WorkspacePage.tsx Add historyEnabled state; pass to sidebar; force mandatory modal after 3rd upload
src/components/AuthModal.tsx Add send-code button, countdown, verification code field to signup tab
src/lib/authService.ts Add sendEmailCode() method; update register() to send code
src/types/api.ts Add SendEmailCodeRequest; add code to RegisterRequest
src/lib/translations.ts Add new i18n keys for history toggle, verification code flow

Task 1: Add i18n strings for new features

Files:

  • Modify: src/lib/translations.ts

  • Step 1: Add new keys to both en and zh auth sections and sidebar section

In src/lib/translations.ts, find the en.auth block and add after oauthFailed:

      sendCode: 'Send Code',
      resendCode: 'Resend',
      codeSent: 'Code sent',
      verificationCode: 'Verification Code',
      verificationCodePlaceholder: 'Enter 6-digit code',
      verificationCodeRequired: 'Please enter the verification code.',
      verificationCodeHint: 'Check your inbox for the 6-digit code.',
      sendCodeFailed: 'Failed to send verification code, please retry.',

In en.sidebar, add after historyHeader:

      historyToggle: 'Show History',
      historyLoginRequired: 'Login to enable history',

In zh.auth, add after oauthFailed:

      sendCode: '发送验证码',
      resendCode: '重新发送',
      codeSent: '验证码已发送',
      verificationCode: '验证码',
      verificationCodePlaceholder: '请输入 6 位验证码',
      verificationCodeRequired: '请输入验证码。',
      verificationCodeHint: '请查收邮箱中的 6 位验证码。',
      sendCodeFailed: '发送验证码失败,请重试。',

In zh.sidebar, add after historyHeader:

      historyToggle: '显示历史',
      historyLoginRequired: '登录后开启历史记录',
  • Step 2: Commit
git add src/lib/translations.ts
git commit -m "feat: add i18n keys for history toggle and email verification"

Task 2: Remove logo from AppNavbar

Files:

  • Modify: src/components/layout/AppNavbar.tsx:38-50

  • Step 1: Remove logo image, text, and divider — keep only the Home icon link

Replace the entire {/* Left: Logo + Home link */} div (lines 3851) with:

      {/* Left: Home link */}
      <div className="flex items-center gap-2">
        <Link
          to="/"
          className="flex items-center gap-1.5 px-2 py-1 text-ink-muted hover:text-ink-secondary text-xs font-medium transition-colors rounded-md hover:bg-cream-200/60"
        >
          <Home size={13} />
          <span className="hidden sm:inline">{t.marketing.nav.home}</span>
        </Link>
      </div>
  • Step 2: Verify dev server renders correctly
npm run dev

Navigate to /app. Confirm the navbar no longer shows the TexPixel icon or text, only the Home link on the left.

  • Step 3: Commit
git add src/components/layout/AppNavbar.tsx
git commit -m "feat: remove logo from workspace navbar"

Task 3: Remove login button from LeftSidebar and add history toggle prop

Files:

  • Modify: src/components/LeftSidebar.tsx

  • Step 1: Add historyEnabled and onToggleHistory to the props interface

Replace the existing LeftSidebarProps interface with:

interface LeftSidebarProps {
  files: FileRecord[];
  selectedFileId: string | null;
  onFileSelect: (fileId: string) => void;
  onUploadClick: () => void;
  canUploadAnonymously: boolean;
  onRequireAuth: () => void;
  isCollapsed: boolean;
  onToggleCollapse: () => void;
  onUploadFiles: (files: File[]) => void;
  hasMore: boolean;
  loadingMore: boolean;
  onLoadMore: () => void;
  historyEnabled: boolean;
  onToggleHistory: () => void;
}
  • Step 2: Destructure the two new props in the function signature

Replace the destructuring block (the function params) to add historyEnabled and onToggleHistory:

export default function LeftSidebar({
  files,
  selectedFileId,
  onFileSelect,
  onUploadClick,
  canUploadAnonymously,
  onRequireAuth,
  isCollapsed,
  onToggleCollapse,
  onUploadFiles,
  hasMore,
  loadingMore,
  onLoadMore,
  historyEnabled,
  onToggleHistory,
}: LeftSidebarProps) {
  • Step 3: Add Toggle icon to lucide imports

Change the import line at the top from:

import { Upload, LogIn, LogOut, FileText, Clock, ChevronLeft, ChevronRight, History, MousePointerClick, FileUp, ClipboardPaste, Loader2 } from 'lucide-react';

to:

import { Upload, LogOut, FileText, Clock, ChevronLeft, ChevronRight, MousePointerClick, FileUp, ClipboardPaste, Loader2 } from 'lucide-react';

(Remove LogIn, History — no longer used in expanded view)

  • Step 4: Remove the showAuthModal state and the AuthModal import/usage from LeftSidebar

Remove these lines near the top of the function body:

  const [showAuthModal, setShowAuthModal] = useState(false);

and the useEffect that sets showAuthModal to false on auth:

  useEffect(() => {
    if (isAuthenticated) {
      setShowAuthModal(false);
    }
  }, [isAuthenticated]);

Also remove import AuthModal from './AuthModal'; at the top of the file, and at the bottom remove the {showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />} JSX.

  • Step 5: Replace the history header section with a toggle switch

Find the {/* Middle Area: History */} block. Replace the header div (the one with Clock icon and historyHeader text) with:

          <div className="flex items-center justify-between text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 px-2">
            <div className="flex items-center gap-2">
              <Clock size={14} />
              <span>{t.sidebar.historyHeader}</span>
            </div>
            <button
              onClick={onToggleHistory}
              className={`relative w-8 h-4 rounded-full transition-colors duration-200 focus:outline-none ${
                historyEnabled ? 'bg-blue-500' : 'bg-gray-300'
              }`}
              title={t.sidebar.historyToggle}
              aria-pressed={historyEnabled}
            >
              <span
                className={`absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full shadow transition-transform duration-200 ${
                  historyEnabled ? 'translate-x-4' : 'translate-x-0'
                }`}
              />
            </button>
          </div>
  • Step 6: Gate the history list behind historyEnabled

Replace the entire <div ref={listRef} ...> scrollable list with:

          <div
            ref={listRef}
            onScroll={handleScroll}
            className="flex-1 overflow-y-auto space-y-1 pr-2 -mr-2 custom-scrollbar"
          >
            {!historyEnabled ? (
              <div className="text-center py-12 text-gray-400 text-sm">
                <div className="mb-2 opacity-50"><FileText size={40} className="mx-auto" /></div>
                {t.sidebar.historyToggle}
              </div>
            ) : !user ? (
              <div className="text-center py-12 text-gray-400 text-sm">
                <div className="mb-2 opacity-50"><FileText size={40} className="mx-auto" /></div>
                {t.sidebar.historyLoginRequired}
              </div>
            ) : files.length === 0 ? (
              <div className="text-center py-12 text-gray-400 text-sm">
                <div className="mb-2 opacity-50"><FileText size={40} className="mx-auto" /></div>
                {t.sidebar.noHistory}
              </div>
            ) : (
              <>
                {files.map((file) => (
                  <button
                    key={file.id}
                    onClick={() => onFileSelect(file.id)}
                    className={`w-full p-3 rounded-lg text-left transition-all border group relative ${selectedFileId === file.id
                      ? 'bg-blue-50 border-blue-200 shadow-sm'
                      : 'bg-white border-transparent hover:bg-gray-50 hover:border-gray-100'
                      }`}
                  >
                    <div className="flex items-start gap-3">
                      <div className={`p-2 rounded-lg ${selectedFileId === file.id ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-500 group-hover:bg-gray-200'}`}>
                        <FileText size={18} />
                      </div>
                      <div className="flex-1 min-w-0">
                        <p className={`text-sm font-medium truncate ${selectedFileId === file.id ? 'text-blue-900' : 'text-gray-700'}`}>
                          {file.filename}
                        </p>
                        <div className="flex items-center gap-2 mt-1">
                          <span className="text-xs text-gray-400">
                            {new Date(file.created_at).toLocaleDateString()}
                          </span>
                          <span className={`w-1.5 h-1.5 rounded-full ${file.status === 'completed' ? 'bg-green-500' :
                            file.status === 'processing' ? 'bg-yellow-500' : 'bg-red-500'
                            }`} />
                        </div>
                      </div>
                    </div>
                  </button>
                ))}
                {loadingMore && (
                  <div className="flex items-center justify-center py-3 text-gray-400">
                    <Loader2 size={18} className="animate-spin" />
                    <span className="ml-2 text-xs">{t.common.loading}</span>
                  </div>
                )}
                {!hasMore && files.length > 0 && (
                  <div className="text-center py-3 text-xs text-gray-400">
                    {t.sidebar.noMore}
                  </div>
                )}
              </>
            )}
          </div>
  • Step 7: Replace the bottom user/login area — remove login button, keep only logged-in user view

Replace the entire {/* Bottom Area: User/Login */} div with:

        {/* Bottom Area: User info (only shown when logged in) */}
        {user && (
          <div className="p-4 border-t border-gray-100 bg-gray-50/30">
            <div className="flex items-center gap-3 p-2 rounded-lg bg-white border border-gray-100 shadow-sm">
              <div className="w-8 h-8 bg-gray-900 rounded-full flex items-center justify-center flex-shrink-0">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                  <path d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z" fill="white" />
                </svg>
              </div>
              <div className="flex-1 min-w-0">
                <p className="text-sm font-medium text-gray-900 truncate">{displayName}</p>
              </div>
              <button
                onClick={() => signOut()}
                className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
                title={t.common.logout}
              >
                <LogOut size={16} />
              </button>
            </div>
          </div>
        )}
  • Step 8: Fix collapsed view — remove LogIn icon button

In the if (isCollapsed) return block, remove the bottom <button> that shows the LogIn icon:

        <button
          onClick={() => !user && setShowAuthModal(true)}
          className="p-3 rounded-lg text-gray-600 hover:bg-gray-200 transition-colors mt-auto"
          title={user ? 'Signed In' : t.common.login}
        >
          <LogIn size={20} />
        </button>

Replace with nothing (delete that button entirely). The isAuthenticated import can stay for now.

  • Step 9: Commit
git add src/components/LeftSidebar.tsx
git commit -m "feat: remove login button from sidebar, add history toggle"

Task 4: Wire history toggle state and mandatory auth in WorkspacePage

Files:

  • Modify: src/pages/WorkspacePage.tsx

  • Step 1: Add historyEnabled state

After the existing const [loadingMore, setLoadingMore] = useState(false); line, add:

  const [historyEnabled, setHistoryEnabled] = useState(false);
  • Step 2: Add handleToggleHistory callback

After the openAuthModal callback, add:

  const handleToggleHistory = useCallback(() => {
    if (!historyEnabled) {
      // Turning on
      if (!user) {
        openAuthModal();
        return;
      }
      setHistoryEnabled(true);
      if (!hasLoadedFiles.current) {
        hasLoadedFiles.current = true;
        loadFiles();
      }
    } else {
      setHistoryEnabled(false);
    }
  }, [historyEnabled, user, openAuthModal]);
  • Step 3: Remove auto-load on auth — history is now opt-in

Find the useEffect that auto-calls loadFiles() when user becomes available:

  useEffect(() => {
    if (!initializing && user && !hasLoadedFiles.current) {
      hasLoadedFiles.current = true;
      loadFiles();
    }
    if (!user) {
      hasLoadedFiles.current = false;
      setFiles([]);
      setSelectedFileId(null);
      setCurrentPage(1);
      setHasMore(false);
    }
  }, [initializing, user]);

Replace with (keep the reset on logout, remove the auto-load):

  useEffect(() => {
    if (!user) {
      hasLoadedFiles.current = false;
      setHistoryEnabled(false);
      setFiles([]);
      setSelectedFileId(null);
      setCurrentPage(1);
      setHasMore(false);
    }
  }, [user]);
  • Step 4: Make the auth modal mandatory after 3rd upload

Add a new state after showAuthModal:

  const [authModalMandatory, setAuthModalMandatory] = useState(false);

Change openAuthModal to accept an optional mandatory parameter:

  const openAuthModal = useCallback((mandatory = false) => {
    setAuthModalMandatory(mandatory);
    setShowAuthModal(true);
  }, []);

In handleUpload, after incrementGuestUsage(), add a mandatory modal trigger when the new count hits the limit:

      if (!user && successfulUploads > 0) {
        incrementGuestUsage();
        // Force login after hitting the limit
        setGuestUsageCount(prev => {
          const next = prev + 1;
          if (next >= GUEST_USAGE_LIMIT) {
            openAuthModal(true);
          }
          return next;
        });
      }

Wait — incrementGuestUsage already increments. We need to check the new count after increment. Replace the if (!user && successfulUploads > 0) block at the end of handleUpload with:

      if (!user && successfulUploads > 0) {
        const nextCount = guestUsageCount + successfulUploads;
        const newCount = Math.min(nextCount, GUEST_USAGE_LIMIT + 10);
        localStorage.setItem(GUEST_USAGE_COUNT_KEY, String(newCount));
        setGuestUsageCount(newCount);
        if (newCount >= GUEST_USAGE_LIMIT) {
          openAuthModal(true);
        }
      }

And remove the separate incrementGuestUsage call entirely (delete the incrementGuestUsage callback too).

  • Step 5: Update the two places that check upload limit to pass mandatory=false (keep non-mandatory for pre-upload checks)

The onUploadClick handler and paste handler already call openAuthModal() with no arg (defaults to false) — that stays as-is.

  • Step 6: Update AuthModal JSX to pass mandatory prop and update close handler

Find {showAuthModal && (<AuthModal onClose={() => setShowAuthModal(false)} />)} and replace with:

      {showAuthModal && (
        <AuthModal
          onClose={() => { setShowAuthModal(false); setAuthModalMandatory(false); }}
          mandatory={authModalMandatory}
        />
      )}
  • Step 7: Pass historyEnabled and onToggleHistory to LeftSidebar

Find the <LeftSidebar JSX and add the two new props:

          historyEnabled={historyEnabled}
          onToggleHistory={handleToggleHistory}
  • Step 8: After login via mandatory modal, auto-enable history

In the useEffect that watches user, add history auto-enable when user logs in while historyEnabled is still false — actually simpler to auto-enable history when user is set and they just came from a mandatory flow. Instead, just handle this in the useEffect that watches user change after modal closes:

Replace the logout-only effect from Step 3 with:

  useEffect(() => {
    if (user && !hasLoadedFiles.current && historyEnabled) {
      // user logged in while history was already toggled on
      hasLoadedFiles.current = true;
      loadFiles();
    }
    if (!user) {
      hasLoadedFiles.current = false;
      setHistoryEnabled(false);
      setFiles([]);
      setSelectedFileId(null);
      setCurrentPage(1);
      setHasMore(false);
    }
  }, [user]);
  • Step 9: Commit
git add src/pages/WorkspacePage.tsx
git commit -m "feat: history toggle state, mandatory auth after 3 guest uploads"

Task 5: Add email verification to API types and authService

Files:

  • Modify: src/types/api.ts

  • Modify: src/lib/authService.ts

  • Step 1: Update RegisterRequest and add SendEmailCodeRequest in src/types/api.ts

Find the existing RegisterRequest interface:

export interface RegisterRequest {
  email: string;
  password: string;
}

Replace with:

export interface RegisterRequest {
  email: string;
  password: string;
  code: string;
}

export interface SendEmailCodeRequest {
  email: string;
}
  • Step 2: Add sendEmailCode() to authService in src/lib/authService.ts

Add the import of SendEmailCodeRequest — it will be used below. Then add a new method inside export const authService = { ... } after the login method:

  async sendEmailCode(email: string): Promise<void> {
    await http.post<null>('/user/email/code', { email } satisfies SendEmailCodeRequest, { skipAuth: true });
  },

Also update the register method signature to accept code:

  async register(credentials: RegisterRequest): Promise<{ user: UserInfo; token: string; expiresAt: number }> {
    const response = await http.post<AuthData>('/user/register', credentials, { skipAuth: true });

    if (!response.data) {
      throw new ApiError(-1, '注册失败,请重试');
    }

    return buildSession(response.data, credentials.email);
  },

(Signature stays the same — just ensuring credentials now includes code via the updated type.)

  • Step 3: Update the import in authService.ts to include SendEmailCodeRequest

Find:

import type {
  AuthData,
  GoogleAuthUrlData,
  GoogleOAuthCallbackRequest,
  LoginRequest,
  RegisterRequest,
  UserInfoData,
  UserInfo,
} from '../types/api';

Replace with:

import type {
  AuthData,
  GoogleAuthUrlData,
  GoogleOAuthCallbackRequest,
  LoginRequest,
  RegisterRequest,
  SendEmailCodeRequest,
  UserInfoData,
  UserInfo,
} from '../types/api';
  • Step 4: Commit
git add src/types/api.ts src/lib/authService.ts
git commit -m "feat: add sendEmailCode API and code field to RegisterRequest"

Task 6: Update AuthContext to pass code through signUp

Files:

  • Modify: src/contexts/AuthContext.tsx

  • Step 1: Update signUp to accept and forward code

Find the signUp function/action in AuthContext.tsx. It currently calls authService.register({ email, password }). Update the signUp signature to accept a third code parameter and pass it:

signUp: async (email: string, password: string, code: string) => { ... }

Inside, change:

await authService.register({ email, password, code });

Also update the AuthContextValue interface (or wherever signUp type is declared) to:

signUp: (email: string, password: string, code: string) => Promise<{ error: string | null }>;
  • Step 2: Commit
git add src/contexts/AuthContext.tsx
git commit -m "feat: thread verification code through AuthContext signUp"

Task 7: Add email verification UI to AuthModal

Files:

  • Modify: src/components/AuthModal.tsx

  • Step 1: Add verification code state variables

After the existing useState declarations in AuthModal, add:

  const [verificationCode, setVerificationCode] = useState('');
  const [codeSent, setCodeSent] = useState(false);
  const [codeCountdown, setCodeCountdown] = useState(0);
  const [sendingCode, setSendingCode] = useState(false);
  const countdownRef = useRef<NodeJS.Timeout | null>(null);

Add useRef to the React import if not already there.

Also import authService at the top:

import { authService } from '../lib/authService';
  • Step 2: Add sendCode handler

After the handleGoogleOAuth function, add:

  const handleSendCode = async () => {
    const normalizedEmail = email.trim();
    if (!normalizedEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
      setFieldErrors(prev => ({ ...prev, email: t.auth.emailInvalid }));
      return;
    }
    setSendingCode(true);
    try {
      await authService.sendEmailCode(normalizedEmail);
      setCodeSent(true);
      setCodeCountdown(60);
      countdownRef.current = setInterval(() => {
        setCodeCountdown(prev => {
          if (prev <= 1) {
            clearInterval(countdownRef.current!);
            return 0;
          }
          return prev - 1;
        });
      }, 1000);
    } catch {
      setLocalError(t.auth.sendCodeFailed);
    } finally {
      setSendingCode(false);
    }
  };

Add a cleanup useEffect for the countdown interval:

  useEffect(() => {
    return () => {
      if (countdownRef.current) clearInterval(countdownRef.current);
    };
  }, []);
  • Step 3: Update handleSubmit to validate code and pass it to signUp

In the handleSubmit function, inside the if (mode === 'signup') validation block, add a check for the verification code:

      if (!verificationCode.trim()) {
        nextFieldErrors.verificationCode = t.auth.verificationCodeRequired;
      }

Update fieldErrors type to include verificationCode:

  const [fieldErrors, setFieldErrors] = useState<{
    email?: string;
    password?: string;
    confirmPassword?: string;
    verificationCode?: string;
  }>({});

Update the signUp call to pass the code:

    const result = mode === 'signup'
      ? await signUp(normalizedEmail, password, verificationCode.trim())
      : await signIn(normalizedEmail, password);
  • Step 4: Add Send Code button inline with the email field (signup mode only)

In the JSX, find the email <div style={s.fieldGroup}> block. Replace it with a version that adds the send-code button when in signup mode:

          <div style={s.fieldGroup}>
            <label htmlFor="auth-email" style={s.label}>{t.auth.email}</label>
            <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
              <div style={{ flex: 1 }}>
                <input
                  id="auth-email"
                  type="email"
                  value={email}
                  onChange={(e) => {
                    setEmail(e.target.value);
                    if (fieldErrors.email) setFieldErrors((prev) => ({ ...prev, email: undefined }));
                  }}
                  style={fieldErrors.email ? { ...s.input, ...s.inputError } : s.input}
                  placeholder="your@email.com"
                  required
                  disabled={isBusy}
                />
                {fieldErrors.email && <p style={s.fieldError}>{fieldErrors.email}</p>}
                {mode === 'signup' && <p style={s.fieldHint}>{t.auth.emailHint}</p>}
              </div>
              {mode === 'signup' && (
                <button
                  type="button"
                  onClick={handleSendCode}
                  disabled={isBusy || sendingCode || codeCountdown > 0}
                  style={{
                    flexShrink: 0,
                    height: '42px',
                    padding: '0 12px',
                    background: codeCountdown > 0 ? '#F5DDD0' : '#C8622A',
                    color: codeCountdown > 0 ? '#AA9685' : 'white',
                    border: 'none',
                    borderRadius: '12px',
                    fontSize: '13px',
                    fontWeight: 600,
                    fontFamily: "'DM Sans', sans-serif",
                    cursor: codeCountdown > 0 || sendingCode ? 'not-allowed' : 'pointer',
                    whiteSpace: 'nowrap',
                    transition: 'all 0.2s',
                  }}
                >
                  {codeCountdown > 0
                    ? `${codeCountdown}s`
                    : codeSent
                    ? t.auth.resendCode
                    : t.auth.sendCode}
                </button>
              )}
            </div>
          </div>
  • Step 5: Add the verification code input field (signup mode, after confirm password)

After the confirm-password {mode === 'signup' && (...)} block, add:

          {mode === 'signup' && (
            <div style={s.fieldGroup}>
              <label htmlFor="auth-code" style={s.label}>{t.auth.verificationCode}</label>
              <input
                id="auth-code"
                type="text"
                inputMode="numeric"
                maxLength={6}
                value={verificationCode}
                onChange={(e) => {
                  setVerificationCode(e.target.value.replace(/\D/g, ''));
                  if (fieldErrors.verificationCode) setFieldErrors(prev => ({ ...prev, verificationCode: undefined }));
                }}
                style={fieldErrors.verificationCode ? { ...s.input, ...s.inputError } : s.input}
                placeholder={t.auth.verificationCodePlaceholder}
                disabled={isBusy}
              />
              {fieldErrors.verificationCode
                ? <p style={s.fieldError}>{fieldErrors.verificationCode}</p>
                : <p style={s.fieldHint}>{t.auth.verificationCodeHint}</p>
              }
            </div>
          )}
  • Step 6: Reset code state when switching to signin tab

In the setMode('signin') onClick handler, add resets:

onClick={() => {
  setMode('signin');
  setFieldErrors({});
  setLocalError('');
  setVerificationCode('');
  setCodeSent(false);
  setCodeCountdown(0);
  if (countdownRef.current) clearInterval(countdownRef.current);
}}
  • Step 7: Verify in browser — register flow should now require sending code first
npm run dev

Open /app, click register. Confirm:

  1. Email field has "Send Code" button
  2. Clicking it (with valid email) triggers send and starts countdown
  3. Verification code field appears below confirm-password
  4. Submitting without code shows validation error
  5. Login tab does not show the code field
  • Step 8: Commit
git add src/components/AuthModal.tsx
git commit -m "feat: add email verification code step to registration flow"

Task 8: Final smoke test

  • Step 1: Run type check
npm run typecheck

Expected: no errors

  • Step 2: Run unit tests
npm run test

Expected: all pass (existing tests should still pass)

  • Step 3: Manual end-to-end check
npm run dev

Verify:

  1. Workspace navbar has no logo — only Home link on left
  2. Sidebar bottom shows no login button when logged out
  3. History toggle is off by default, shows placeholder text
  4. Toggling history on while logged out opens auth modal
  5. After 3 guest uploads, mandatory auth modal appears (no close button)
  6. Register tab: Send Code button appears, countdown works, code field required
  7. Sign-in tab: no code field visible
  8. Logged-in user: history toggle on loads history; logout resets toggle to off