diff --git a/components/chat-footer-message.tsx b/components/chat-footer-message.tsx
new file mode 100644
index 000000000..51e6c2a06
--- /dev/null
+++ b/components/chat-footer-message.tsx
@@ -0,0 +1,58 @@
+'use client'
+
+import { useMemo } from 'react'
+
+import { DISCLAIMER_TEXT, getTips, shuffle } from '@/lib/footer-tips'
+
+import { useTypewriterCycle } from '@/hooks/use-typewriter-cycle'
+
+function FooterContent() {
+ const shuffledTips = useMemo(() => shuffle(getTips()), [])
+
+ // Build items array: disclaimer first, then tip descriptions
+ const items = useMemo(
+ () => [DISCLAIMER_TEXT, ...shuffledTips.map(t => t.description)],
+ [shuffledTips]
+ )
+
+ const { currentIndex, charCount, displayText, isIdle } =
+ useTypewriterCycle(items)
+
+ if (isIdle && charCount === 0) return null
+
+ // Index 0 = disclaimer, 1+ = tips
+ const tipDataIndex = currentIndex - 1
+ const isTip = tipDataIndex >= 0 && charCount > 0
+ const tip = isTip ? shuffledTips[tipDataIndex] : null
+
+ return (
+
+ {tip ? (
+
+ Tips:
+
+ {tip.keys.map((key, i) => (
+
+ {key}
+
+ ))}
+
+ {displayText}
+
+ ) : (
+
+ {displayText}
+
+ )}
+
+ )
+}
+
+export function ChatFooterMessage({ isLoading }: { isLoading: boolean }) {
+ if (isLoading) return null
+
+ return
+}
diff --git a/components/chat-messages.tsx b/components/chat-messages.tsx
index a5aa03ce3..c2b52e164 100644
--- a/components/chat-messages.tsx
+++ b/components/chat-messages.tsx
@@ -11,6 +11,7 @@ import { extractCitationMapsFromMessages } from '@/lib/utils/citation'
import { AnimatedLogo } from './ui/animated-logo'
import { ChatError } from './chat-error'
+import { ChatFooterMessage } from './chat-footer-message'
import { RenderMessage } from './render-message'
// Import section structure interface
@@ -228,10 +229,14 @@ export function ChatMessages({
)
})}
- {/* Show assistant logo after assistant messages */}
+ {/* Show assistant logo and footer message after assistant messages */}
{showAssistantLogo && sectionIndex === sections.length - 1 && (
-
-
+
)}
{sectionIndex === sections.length - 1 && (
diff --git a/hooks/use-typewriter-cycle.ts b/hooks/use-typewriter-cycle.ts
new file mode 100644
index 000000000..02abf31f9
--- /dev/null
+++ b/hooks/use-typewriter-cycle.ts
@@ -0,0 +1,138 @@
+'use client'
+
+import { useEffect, useRef, useState } from 'react'
+
+type Stage =
+ | 'init'
+ | 'typing-in'
+ | 'visible'
+ | 'typing-out'
+ | 'switching'
+ | 'idle'
+
+export interface TypewriterCycleOptions {
+ /** Duration to show the first item (ms) */
+ firstDuration?: number
+ /** Duration to show each subsequent item (ms) */
+ itemDuration?: number
+ /** Idle pause between items (ms) */
+ idleDuration?: number
+ /** Typing speed per character (ms) */
+ charInterval?: number
+ /** Delay before first item appears (ms) */
+ initialDelay?: number
+}
+
+const DEFAULTS: Required
= {
+ firstDuration: 5000,
+ itemDuration: 15000,
+ idleDuration: 15000,
+ charInterval: 25,
+ initialDelay: 300
+}
+
+export function useTypewriterCycle(
+ items: string[],
+ options?: TypewriterCycleOptions
+) {
+ const {
+ firstDuration,
+ itemDuration,
+ idleDuration,
+ charInterval,
+ initialDelay
+ } = { ...DEFAULTS, ...options }
+
+ const [stage, setStage] = useState('init')
+ const [currentIndex, setCurrentIndex] = useState(0)
+ const [charCount, setCharCount] = useState(0)
+ const timerRef = useRef | null>(null)
+ const intervalRef = useRef | null>(null)
+
+ const currentText = items[currentIndex] ?? ''
+
+ const clearTimers = () => {
+ if (timerRef.current) clearTimeout(timerRef.current)
+ if (intervalRef.current) clearInterval(intervalRef.current)
+ }
+
+ useEffect(() => {
+ clearTimers()
+
+ switch (stage) {
+ case 'init':
+ timerRef.current = setTimeout(() => {
+ setCharCount(0)
+ setStage('typing-in')
+ }, initialDelay)
+ break
+
+ case 'typing-in':
+ intervalRef.current = setInterval(() => {
+ setCharCount(prev => {
+ const next = prev + 1
+ if (next >= currentText.length) {
+ if (intervalRef.current) clearInterval(intervalRef.current)
+ setStage('visible')
+ }
+ return next
+ })
+ }, charInterval)
+ break
+
+ case 'visible': {
+ const duration = currentIndex === 0 ? firstDuration : itemDuration
+ timerRef.current = setTimeout(() => {
+ setStage('typing-out')
+ }, duration)
+ break
+ }
+
+ case 'typing-out':
+ intervalRef.current = setInterval(() => {
+ setCharCount(prev => {
+ const next = prev - 1
+ if (next <= 0) {
+ if (intervalRef.current) clearInterval(intervalRef.current)
+ setStage('switching')
+ }
+ return Math.max(0, next)
+ })
+ }, charInterval)
+ break
+
+ case 'switching':
+ timerRef.current = setTimeout(() => {
+ setCurrentIndex(p => (p + 1) % items.length)
+ setCharCount(0)
+ setStage('idle')
+ }, 50)
+ break
+
+ case 'idle':
+ timerRef.current = setTimeout(() => {
+ setStage('typing-in')
+ }, idleDuration)
+ break
+ }
+
+ return clearTimers
+ }, [
+ stage,
+ currentIndex,
+ items,
+ currentText.length,
+ initialDelay,
+ charInterval,
+ firstDuration,
+ itemDuration,
+ idleDuration
+ ])
+
+ return {
+ currentIndex,
+ charCount,
+ displayText: currentText.slice(0, charCount),
+ isIdle: stage === 'idle' || stage === 'init' || stage === 'switching'
+ }
+}
diff --git a/lib/footer-tips.ts b/lib/footer-tips.ts
new file mode 100644
index 000000000..01bbfb105
--- /dev/null
+++ b/lib/footer-tips.ts
@@ -0,0 +1,36 @@
+import { formatShortcutKeys, SHORTCUTS } from '@/lib/keyboard-shortcuts'
+
+export const DISCLAIMER_TEXT =
+ 'Morphic can make mistakes. Please double-check responses.'
+
+const SHORTCUT_ENTRIES = [
+ SHORTCUTS.newChat,
+ SHORTCUTS.copyMessage,
+ SHORTCUTS.toggleSidebar,
+ SHORTCUTS.toggleTheme,
+ SHORTCUTS.toggleSearchMode,
+ SHORTCUTS.showShortcuts
+] as const
+
+export type Tip = { keys: string[]; description: string }
+
+// Lazily initialized to avoid hydration mismatch from navigator check
+let cachedTips: Tip[] | null = null
+export function getTips(): Tip[] {
+ if (!cachedTips) {
+ cachedTips = SHORTCUT_ENTRIES.map(s => ({
+ keys: formatShortcutKeys(s),
+ description: s.description
+ }))
+ }
+ return cachedTips
+}
+
+export function shuffle(arr: T[]): T[] {
+ const a = [...arr]
+ for (let i = a.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1))
+ ;[a[i], a[j]] = [a[j], a[i]]
+ }
+ return a
+}