{"slug":"incognito-toggle","category":"composer","job":"trust","added":"2026-11-07T22:47Z","title":"Incognito Toggle","description":"A header control that starts a temporary chat, with a persistent banner stating exactly what won't be saved, remembered or used for training.","url":"https://ai-patterns.app/patterns/composer/incognito-toggle","markdown":"https://ai-patterns.app/patterns/composer/incognito-toggle.md","json":"https://ai-patterns.app/api/patterns/composer/incognito-toggle","seenIn":[{"team":"chatgpt","source":"https://help.openai.com/en/articles/8914046-temporary-chat-faq"},{"team":"Google Gemini","source":"https://blog.google/products-and-platforms/products/gemini/temporary-chats-privacy-controls/"},{"team":"Perplexity","source":"https://www.perplexity.ai/help-center/en/articles/12639758-incognito-mode-troubleshooting"}],"spec":"# Incognito Toggle\n\n## Summary\nA header control that turns the current chat into a temporary one, plus a banner that stays on screen for as long as it lasts and says exactly what is and isn't kept. It lets people talk to the assistant without the conversation landing in their history, memory or training data.\n\n## When to use\n- Assistants with history, memory or training on conversations, where some topics (health, money, a surprise gift) shouldn't stick.\n- Shared or work accounts where a user wants to try something without it showing up for others.\n- Testing a prompt without the assistant's memory colouring the answer.\n\n## When not to use\n- Products that don't keep conversations at all — there is nothing to turn off, so the toggle would mislead.\n- As a substitute for real data controls. Pair it with Memory Panel and a data-retention setting.\n- Mid-conversation, after messages have already been saved. Start a new temporary chat instead of converting an existing one.\n\n## Anatomy\n- Toggle: a pill button with an eye-off mark and a label, pressed when temporary mode is on. Can collapse to icon-only.\n- Banner: icon, title, a specific description of what is kept, and an Exit button.\n- Visual mode cue: the composer takes a dashed border and changes its placeholder, so the mode is obvious even when the banner scrolls away.\n\n## Behavior\n- The toggle is offered on a new, empty chat. Turning it on shows the banner and restyles the composer immediately.\n- The banner stays for the whole temporary chat; it is not a dismissible toast.\n- Exit (or turning the toggle off) ends temporary mode and starts a fresh normal chat; the temporary conversation is discarded, so confirm first if it has messages.\n- While temporary, the assistant doesn't read or write memory, and the chat never appears in history or search.\n\n## Content guidelines\n- Name the mode plainly: \"Temporary chat\". Avoid \"Incognito\" in user-facing copy unless your brand already uses it — people confuse it with browser privacy.\n- State the actual guarantees and any exceptions: \"won't appear in history, won't update memory, won't be used for training. May be kept for up to 30 days for safety.\"\n- Never promise more privacy than the system delivers.\n\n## Accessibility\n- The toggle is a button with `aria-pressed`; in icon-only form it carries an `aria-label` and tooltip.\n- The banner is `role=\"status\"`, so turning the mode on is announced.\n- The mode is shown with text and border style, not colour alone.\n\n## Related patterns\n- **Memory Panel** — Use it as well to review and delete what was remembered, while this toggle stops remembering in the first place.\n- **AI Disclosure Badge** — Use it as well when people also need to know which content the AI made, not just what it will keep.\n- **Prompt Bar** — Use it as well, since this mode restyles the composer and uses its placeholder for the mode hint.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { EyeOff } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface IncognitoToggleProps {\n  active: boolean;\n  onActiveChange: (active: boolean) => void;\n  label?: string;\n  /** Hide the label and show the icon only, e.g. in a crowded header. */\n  iconOnly?: boolean;\n  className?: string;\n}\n\n/** The header control that starts or ends a temporary chat. */\nexport function IncognitoToggle({\n  active,\n  onActiveChange,\n  label = \"Temporary chat\",\n  iconOnly,\n  className,\n}: IncognitoToggleProps) {\n  return (\n    <button\n      type=\"button\"\n      aria-pressed={active}\n      aria-label={iconOnly ? label : undefined}\n      title={iconOnly ? label : undefined}\n      onClick={() => onActiveChange(!active)}\n      className={cn(\n        \"relative inline-flex h-8 items-center gap-1.5 rounded-full border text-xs font-medium transition-colors\",\n        iconOnly ? \"w-8 justify-center\" : \"px-3\",\n        active\n          ? \"border-foreground bg-foreground text-background\"\n          : \"border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground\",\n        className\n      )}\n    >\n      <motion.span\n        key={String(active)}\n        initial={{ scale: 0.6, rotate: -20, opacity: 0 }}\n        animate={{ scale: 1, rotate: 0, opacity: 1 }}\n        transition={{ type: \"spring\", stiffness: 500, damping: 26 }}\n        className=\"flex\"\n      >\n        <EyeOff className=\"size-3.5\" aria-hidden />\n      </motion.span>\n      {!iconOnly && <span>{label}</span>}\n    </button>\n  );\n}\n\nexport interface TemporaryChatBannerProps {\n  active: boolean;\n  title?: string;\n  /** What is and isn't kept. Be specific: this is the promise users rely on. */\n  description?: string;\n  onExit?: () => void;\n  exitLabel?: string;\n  className?: string;\n}\n\n/** The persistent notice shown for as long as the chat is temporary. */\nexport function TemporaryChatBanner({\n  active,\n  title = \"Temporary chat\",\n  description = \"This chat won't appear in your history, won't update memory and won't be used to train models. It may be kept for up to 30 days for safety.\",\n  onExit,\n  exitLabel = \"Exit\",\n  className,\n}: TemporaryChatBannerProps) {\n  return (\n    <AnimatePresence initial={false}>\n      {active && (\n        <motion.div\n          initial={{ opacity: 0, height: 0 }}\n          animate={{ opacity: 1, height: \"auto\" }}\n          exit={{ opacity: 0, height: 0 }}\n          transition={{ duration: 0.22, ease: \"easeOut\" }}\n          className=\"overflow-hidden\"\n        >\n          <div\n            role=\"status\"\n            className={cn(\n              \"flex items-start gap-3 rounded-2xl border border-dashed border-foreground/30 bg-muted/50 px-4 py-3\",\n              className\n            )}\n          >\n            <span className=\"mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-foreground text-background\">\n              <EyeOff className=\"size-3.5\" aria-hidden />\n            </span>\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"text-sm font-medium\">{title}</p>\n              <p className=\"mt-0.5 text-xs text-pretty text-muted-foreground\">{description}</p>\n            </div>\n            {onExit && (\n              <button\n                type=\"button\"\n                onClick={onExit}\n                className=\"shrink-0 rounded-lg border bg-background px-2.5 py-1 text-xs font-medium transition-colors hover:bg-accent\"\n              >\n                {exitLabel}\n              </button>\n            )}\n          </div>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}