Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Toggle history #369

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# The settings below are essential for the basic functionality of the system.

# Storage Configuration
STORAGE_PROVIDER=redis # Options: 'redis' or 'local' (browser localStorage) - defaults to redis with browser storage fallback
STORAGE_PROVIDER=redis # Options: 'redis' or 'none' - defaults to redis
USE_LOCAL_REDIS=false # Only applies when STORAGE_PROVIDER=redis
LOCAL_REDIS_URL=redis://localhost:6379 # or redis://redis:6379 if you're using docker compose

Expand Down
62 changes: 40 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,27 +241,45 @@ engines:
- llama3-groq-8b-8192-tool-use-preview
- llama3-groq-70b-8192-tool-use-preview

## Storage Configuration

This application supports two storage methods for chat history:

1. **Redis Storage (Default)**
- Persistent server-side storage
- Set `STORAGE_PROVIDER=redis` in your `.env.local` file
- Configure either Upstash Redis or local Redis instance
- Supports cross-device access to chat history

2. **Browser Storage**
- Uses browser's localStorage
- Set `STORAGE_PROVIDER=local` in your `.env.local` file
- Data persists in browser until cleared
- Storage is limited to browser/device
- Useful for development and testing

The application will automatically fall back to browser storage (localStorage) if Redis is unavailable.
## Storage Configuration

**Note**: Browser storage means chat history is:
- Stored in the user's browser
- Limited to the device being used
- Cleared if browser data is cleared
- Not shared across devices or browsers
This application supports two storage configuration options:

1. **Redis Storage (`STORAGE_PROVIDER=redis`)**
- Full Redis functionality with optional chat history
- Configure either Upstash Redis or local Redis instance:
```env
STORAGE_PROVIDER=redis
USE_LOCAL_REDIS=true|false
LOCAL_REDIS_URL=redis://localhost:6379 # For local Redis
# Or for Upstash:
UPSTASH_REDIS_REST_URL=your_url
UPSTASH_REDIS_REST_TOKEN=your_token
```
- Features:
- Persistent server-side storage
- Chat history can be toggled on/off by users
- Cross-device access to chat history
- Redis operations maintained for caching even when history is disabled

2. **No Storage (`STORAGE_PROVIDER=none`)**
- Completely disables Redis operations
- No chat history functionality
- No storage or caching operations
- Suitable for development or when storage is not needed
- Chat history toggle will not be available

### Chat History Control

When Redis storage is enabled (`STORAGE_PROVIDER=redis`):
- Users can toggle chat history on/off through the UI
- When enabled: Chats are saved and accessible from the history panel
- When disabled: Chats are not saved, but Redis remains available for other operations
- History toggle state persists between sessions

When storage is disabled (`STORAGE_PROVIDER=none`):
- Chat history is permanently disabled
- No Redis operations are performed
- UI shows informational message about storage configuration
- All chat functionality works without persistence
36 changes: 12 additions & 24 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ async function submit(
const isGenerating = createStreamableValue(true)
const isCollapsed = createStreamableValue(false)

const redis = await getRedisClient()
const chatHistoryEnabled = await redis.get(
'user:anonymous:chatHistoryEnabled'
)

const aiMessages = [...(retryMessages ?? aiState.get().messages)]
// Get the messages from the state, filter out the tool messages
const messages: CoreMessage[] = aiMessages
Expand Down Expand Up @@ -76,16 +71,8 @@ async function submit(
? 'input_related'
: 'inquiry'

// Always add the user message to the messages array if content is not null
if (content !== null) {
messages.push({
role: 'user',
content
})
}

// Only update aiState if chat history is enabled and content is not null
if (chatHistoryEnabled !== 'false' && content !== null) {
// Always update AIState if content exists
if (content) {
aiState.update({
...aiState.get(),
messages: [
Expand All @@ -98,6 +85,10 @@ async function submit(
}
]
})
messages.push({
role: 'user',
content
})
}

// Run the agent workflow
Expand Down Expand Up @@ -157,19 +148,16 @@ export const AI = createAI<AIState, UIState>({
onSetAIState: async ({ state, done }) => {
'use server'

// Get chat history setting first
const redis = await getRedisClient()
const chatHistoryEnabled = await redis.get(
'user:anonymous:chatHistoryEnabled'
)
const chatHistoryEnabled = await redis.get('user:anonymous:chatHistoryEnabled')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO: Since chatHistoryEnabled is also being checked in saveChat, checking it in either place should be sufficient.

2804b5e#diff-819d2d9016d052cd621d2190deaf60688a87f953233d709b40fdabcf99d83f47R175-R180

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea just put it as additional safety net but see what you mean


// Check if there is any message of type 'answer' in the state messages
if (
!state.messages.some(e => e.type === 'answer') ||
chatHistoryEnabled === 'false'
) {
// Exit early if chat history is disabled or no answer messages
if (chatHistoryEnabled === 'false' || !state.messages.some(e => e.type === 'answer')) {
return
}

// Only proceed with storage operations if chat history is enabled
const { chatId, messages } = state
const createdAt = new Date()
const userId = 'anonymous'
Expand All @@ -179,7 +167,7 @@ export const AI = createAI<AIState, UIState>({
? JSON.parse(messages[0].content)?.input?.substring(0, 100) ||
'Untitled'
: 'Untitled'
// Add an 'end' message at the end to determine if the history needs to be reloaded

const updatedMessages: AIMessage[] = [
...messages,
{
Expand Down
14 changes: 13 additions & 1 deletion components/chat-history-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ import { useChatHistory } from '@/lib/utils/chat-history-context'
import { updateChatHistorySetting } from '@/lib/actions/chat'

export function ChatHistoryToggle() {
const { chatHistoryEnabled, setChatHistoryEnabled } = useChatHistory()
const { chatHistoryEnabled, setChatHistoryEnabled, isLoading, storageAvailable } = useChatHistory()

// If storage is not available, show message instead of toggle
if (!storageAvailable) {
return (
<div className="flex flex-col space-y-2 mb-4 p-4 bg-muted/50 rounded-lg">
<p className="text-sm text-muted-foreground">
Chat history is currently unavailable. To enable history functionality, please configure Redis storage in your environment settings.
</p>
</div>
)
}

const handleToggle = async (checked: boolean) => {
const success = await updateChatHistorySetting('anonymous', checked)
Expand All @@ -22,6 +33,7 @@ export function ChatHistoryToggle() {
id="chat-history"
checked={chatHistoryEnabled}
onCheckedChange={handleToggle}
disabled={isLoading}
/>
<Label htmlFor="chat-history">Enable Chat History</Label>
</div>
Expand Down
9 changes: 4 additions & 5 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,10 @@ export function ChatPanel({ messages, query }: ChatPanelProps) {
// Clear messages
const handleClear = () => {
setIsGenerating(false)
if (chatHistoryEnabled) {
setMessages([])
setAIMessage({ messages: [], chatId: '' })
}
// Always clear input and reset UI state when clearing even if chat history is disabled
setInput('')
setMessages([])
setAIMessage({ messages: [], chatId: generateId() }) // Reset AIState with new chatId
router.push('/')
}

Expand Down Expand Up @@ -149,7 +148,7 @@ export function ChatPanel({ messages, query }: ChatPanelProps) {
placeholder="Ask a question..."
spellCheck={false}
value={input}
className="resize-none w-full min-h-12 rounded-fill bg-muted border border-input pl-4 pr-10 pt-3 pb-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'"
className="resize-none w-full min-h-12 rounded-fill bg-muted border border-input pl-4 pr-10 pt-3 pb-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ring color has changed. I don't want to change the ring color.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok so no cosmetic changes . cool

onChange={e => {
setInput(e.target.value)
setShowEmptyScreen(e.target.value.length === 0)
Expand Down
6 changes: 4 additions & 2 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import HistoryContainer from './history-container'
import TopRightMenu from './ui/top-right-menu'

export const Header: React.FC = async () => {
// Get storage provider setting from environment
const storageProvider = process.env.STORAGE_PROVIDER || 'redis'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header will only be controlled on mobile. If you return null in the history-container, you can control everything at once.

https://github.com/miurla/morphic/blob/main/components/history-container.tsx

  const storageProvider = process.env.STORAGE_PROVIDER
  const enabled = storageProvider !== 'none'

  if (!enabled) {
    return null
  }


return (
<header className="fixed w-full p-1 md:p-2 flex justify-between items-center z-10 backdrop-blur md:backdrop-blur-none bg-background/80 md:bg-transparent">
<div>
Expand All @@ -16,8 +19,7 @@ export const Header: React.FC = async () => {
</div>
<div className="flex gap-0.5">
<ModeToggle />
<HistoryContainer location="header" />
{/* <TopRightMenu /> */}
{storageProvider !== 'none' && <HistoryContainer location="header" />}
</div>
</header>
)
Expand Down
41 changes: 24 additions & 17 deletions components/history-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,33 @@ type HistoryListProps = {
export function HistoryList({ userId, chatHistoryEnabled }: HistoryListProps) {
const [chats, setChats] = useState<Chat[]>([])
const [isLoading, setIsLoading] = useState(true)
const [clearTrigger, setClearTrigger] = useState(0) // Add this to force refresh
const [error, setError] = useState(false)

useEffect(() => {
const loadChats = async () => {
if (chatHistoryEnabled) {
const fetchedChats = await getChats(userId)
setChats(fetchedChats || [])
} else {
if (!chatHistoryEnabled) {
setChats([])
setIsLoading(false)
return
}

try {
const fetchedChats = await getChats(userId)
if (fetchedChats) {
setChats(fetchedChats)
setError(false)
}
} catch (error) {
console.error('Error fetching chats:', error)
setError(true)
// Don't clear existing chats on error
} finally {
setIsLoading(false)
}
setIsLoading(false)
}

loadChats()
}, [userId, chatHistoryEnabled, clearTrigger]) // Add clearTrigger to dependencies

// trigger a refresh
const refreshList = () => {
setClearTrigger(prev => prev + 1)
}

if (isLoading) {
return <div>Loading...</div>
}
}, [userId, chatHistoryEnabled])

if (!chatHistoryEnabled) {
return (
Expand All @@ -47,6 +50,10 @@ export function HistoryList({ userId, chatHistoryEnabled }: HistoryListProps) {
)
}

if (isLoading && chats.length === 0) {
return <div className="text-sm text-center py-4">Loading...</div>
}

return (
<div className="flex flex-col flex-1 space-y-3 h-full">
<div className="flex flex-col space-y-0.5 flex-1 overflow-y-auto">
Expand All @@ -62,7 +69,7 @@ export function HistoryList({ userId, chatHistoryEnabled }: HistoryListProps) {
</div>

<div className="sticky bottom-0 bg-background py-2">
<ClearHistory empty={!chats?.length} onCleared={refreshList} />
<ClearHistory empty={!chats?.length} onCleared={() => setChats([])} />
</div>
</div>
)
Expand Down
20 changes: 9 additions & 11 deletions components/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,17 @@ export function History({ location }: HistoryProps) {
const router = useRouter()
const [isPending, startTransition] = useTransition()
const { isGenerating } = useAppState()
const { chatHistoryEnabled, refreshChatHistory } = useChatHistory()
const { chatHistoryEnabled, refreshChatHistory, storageAvailable } = useChatHistory()

const onOpenChange = async (open: boolean) => {
if (open) {
if (chatHistoryEnabled) {
startTransition(async () => {
try {
await refreshChatHistory()
} catch (error) {
console.error('Failed to refresh chat history:', error)
}
})
}
if (open && chatHistoryEnabled && storageAvailable) {
startTransition(async () => {
try {
await refreshChatHistory()
} catch (error) {
console.error('Failed to refresh chat history:', error)
}
})
}
}

Expand Down
Loading