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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 6 additions & 4 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# Required
# The settings below are essential for the basic functionality of the system.

# Storage Configuration
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

# OpenAI API key retrieved here: https://platform.openai.com/api-keys
OPENAI_API_KEY=[YOUR_OPENAI_API_KEY]

# Tavily API Key retrieved here: https://app.tavily.com/home
TAVILY_API_KEY=[YOUR_TAVILY_API_KEY]

# Redis Configuration
USE_LOCAL_REDIS=false
LOCAL_REDIS_URL=redis://localhost:6379 # or redis://redis:6379 if you're using docker compose

# Upstash Redis URL and Token retrieved here: https://console.upstash.com/redis
# Required only if STORAGE_PROVIDER=redis
UPSTASH_REDIS_REST_URL=[YOUR_UPSTASH_REDIS_REST_URL]
UPSTASH_REDIS_REST_TOKEN=[YOUR_UPSTASH_REDIS_REST_TOKEN]

Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ An AI-powered search engine with a generative UI.
- SearXNG Search API support with customizable depth (basic or advanced)
- Configurable search depth (basic or advanced)
- SearXNG Search API support with customizable depth
- Configurable chat history with toggle functionality
- Flexible storage options (Redis or LocalStorage)
- User-controlled chat history persistence

## 🧱 Stack

Expand Down Expand Up @@ -240,3 +243,46 @@ engines:
- Groq
- llama3-groq-8b-8192-tool-use-preview
- llama3-groq-70b-8192-tool-use-preview


## Storage Configuration

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
16 changes: 11 additions & 5 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { CoreMessage, generateId } from 'ai'
import { Section } from '@/components/section'
import { FollowupPanel } from '@/components/followup-panel'
import { saveChat } from '@/lib/actions/chat'
import { saveChat, getChats } from '@/lib/actions/chat'
import { Chat } from '@/lib/types'
import { AIMessage } from '@/lib/types'
import { UserMessage } from '@/components/user-message'
Expand All @@ -21,6 +21,7 @@ import { VideoSearchSection } from '@/components/video-search-section'
import { AnswerSection } from '@/components/answer-section'
import { workflow } from '@/lib/actions/workflow'
import { isProviderEnabled } from '@/lib/utils/registry'
import { getRedisClient } from '@/lib/redis/config'

const MAX_MESSAGES = 6

Expand Down Expand Up @@ -82,7 +83,7 @@ async function submit(
)
}

// Add the user message to the state
// Always update AIState if content exists
if (content) {
aiState.update({
...aiState.get(),
Expand Down Expand Up @@ -160,11 +161,16 @@ export const AI = createAI<AIState, UIState>({
onSetAIState: async ({ state, done }) => {
'use server'

// Check if there is any message of type 'answer' in the state messages
if (!state.messages.some(e => e.type === 'answer')) {
// Get chat history setting first
const redis = await getRedisClient()
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


// 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 @@ -174,7 +180,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
16 changes: 11 additions & 5 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Footer from '@/components/footer'
import { Sidebar } from '@/components/sidebar'
import { Toaster } from '@/components/ui/sonner'
import { AppStateProvider } from '@/lib/utils/app-state'
import { ChatHistoryProvider } from '@/lib/utils/chat-history-context'
import ClientWrapper from '@/components/client-wrapper'

const fontSans = FontSans({
subsets: ['latin'],
Expand Down Expand Up @@ -56,11 +58,15 @@ export default function RootLayout({
disableTransitionOnChange
>
<AppStateProvider>
<Header />
{children}
<Sidebar />
<Footer />
<Toaster />
<ClientWrapper>
<ChatHistoryProvider>
<Header />
{children}
<Sidebar />
<Footer />
<Toaster />
</ChatHistoryProvider>
</ClientWrapper>
</AppStateProvider>
</ThemeProvider>
</body>
Expand Down
12 changes: 12 additions & 0 deletions app/links/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function LinksPage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center py-2">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Links</h1>
<p className="text-muted-foreground">
Coming soon - Link management and sharing features can be implemented in a future update.
</p>
</div>
</div>
)
}
12 changes: 12 additions & 0 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function LoginPage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center py-2">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Login</h1>
<p className="text-muted-foreground">
Coming soon - User authentication can be implemented in a future update.
</p>
</div>
</div>
)
}
41 changes: 41 additions & 0 deletions components/chat-history-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client'

import React from 'react'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { useChatHistory } from '@/lib/utils/chat-history-context'
import { updateChatHistorySetting } from '@/lib/actions/chat'

export function ChatHistoryToggle() {
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)
if (success) {
setChatHistoryEnabled(checked)
}
}

return (
<div className="flex items-center space-x-2 mb-4">
<Switch
id="chat-history"
checked={chatHistoryEnabled}
onCheckedChange={handleToggle}
disabled={isLoading}
/>
<Label htmlFor="chat-history">Enable Chat History</Label>
</div>
)
}
11 changes: 8 additions & 3 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { models } from '@/lib/types/models'
import { useLocalStorage } from '@/lib/hooks/use-local-storage'
import { getDefaultModelId } from '@/lib/utils'
import { toast } from 'sonner'
import { useChatHistory } from '@/lib/utils/chat-history-context'

interface ChatPanelProps {
messages: UIState
Expand All @@ -25,6 +26,7 @@ interface ChatPanelProps {
}

export function ChatPanel({ messages, query, onModelChange }: ChatPanelProps) {
const { chatHistoryEnabled } = useChatHistory()
const [input, setInput] = useState('')
const [showEmptyScreen, setShowEmptyScreen] = useState(false)
const [, setMessages] = useUIState<typeof AI>()
Expand Down Expand Up @@ -57,7 +59,7 @@ export function ChatPanel({ messages, query, onModelChange }: ChatPanelProps) {
setInput(query)
setIsGenerating(true)

// Add user message to UI state
// Always add user message to UI state
setMessages(currentMessages => [
...currentMessages,
{
Expand All @@ -79,6 +81,8 @@ export function ChatPanel({ messages, query, onModelChange }: ChatPanelProps) {
}

const responseMessage = await submit(data)

// Always update UI with response message
setMessages(currentMessages => [...currentMessages, responseMessage])
}

Expand Down Expand Up @@ -114,9 +118,10 @@ export function ChatPanel({ messages, query, onModelChange }: ChatPanelProps) {
// Clear messages
const handleClear = () => {
setIsGenerating(false)
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
21 changes: 18 additions & 3 deletions components/clear-history.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client'

import { useState, useTransition } from 'react'
import { useRouter } from 'next/navigation'
import { Trash } from 'lucide-react'

import {
AlertDialog,
AlertDialogAction,
Expand All @@ -14,21 +17,31 @@ import {
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { clearChats } from '@/lib/actions/chat'
import { useChatHistory } from '@/lib/utils/chat-history-context'
import { toast } from 'sonner'
import { Spinner } from './ui/spinner'

type ClearHistoryProps = {
empty: boolean
onCleared?: () => void
}

export function ClearHistory({ empty }: ClearHistoryProps) {
export function ClearHistory({ empty, onCleared }: ClearHistoryProps) {
const router = useRouter()
const [open, setOpen] = useState(false)
const [isPending, startTransition] = useTransition()
const { refreshChatHistory } = useChatHistory()

return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>
<Button variant="outline" className="w-full" disabled={empty}>
Clear History
<Button
variant="ghost"
disabled={empty || isPending}
className="w-full justify-start"
>
<Trash className="mr-2 h-4 w-4" />
Clear history
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
Expand All @@ -50,6 +63,8 @@ export function ClearHistory({ empty }: ClearHistoryProps) {
if (result?.error) {
toast.error(result.error)
} else {
await refreshChatHistory()
onCleared?.()
toast.success('History cleared')
}
setOpen(false)
Expand Down
24 changes: 24 additions & 0 deletions components/client-history-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'

import React, { Suspense } from 'react'
import { useChatHistory } from '@/lib/utils/chat-history-context'
import { ChatHistoryToggle } from './chat-history-toggle'
import { HistoryList } from './history-list'

type ClientHistoryListProps = {
userId?: string
}

export default function ClientHistoryList(props: ClientHistoryListProps) {
const { chatHistoryEnabled } = useChatHistory()

return (
<>
<ChatHistoryToggle />
<Suspense fallback={<div>Loading...</div>}>

<HistoryList{...props} chatHistoryEnabled={chatHistoryEnabled} />
</Suspense>
</>
)
}
21 changes: 21 additions & 0 deletions components/client-history-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client'

import React from 'react'
import { useChatHistory } from '@/lib/utils/chat-history-context'
import { ChatHistoryToggle } from './chat-history-toggle'
import { HistoryList } from './history-list'

type ClientHistoryWrapperProps = {
userId?: string
}

export function ClientHistoryWrapper({ userId }: ClientHistoryWrapperProps) {
const { chatHistoryEnabled } = useChatHistory()

return (
<>
<ChatHistoryToggle />
<HistoryList userId={userId} chatHistoryEnabled={chatHistoryEnabled} />
</>
)
}
9 changes: 9 additions & 0 deletions components/client-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use client'

import React from 'react'

const ClientWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <>{children}</>
}

export default ClientWrapper
Loading