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 3 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
12 changes: 7 additions & 5 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 'local' (browser localStorage) - defaults to redis with browser storage fallback
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 Expand Up @@ -76,4 +78,4 @@ SEARXNG_SAFESEARCH=0 # Safe search setting: 0 (off), 1 (moderate), 2 (strict)
# If you want to use Jina instead of Tavily for retrieve tool, enable the following variables.
# JINA_API_KEY=[YOUR_JINA_API_KEY]

#NEXT_PUBLIC_BASE_URL=http://localhost:3000
#NEXT_PUBLIC_BASE_URL=http://localhost:3000
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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 @@ -237,3 +240,28 @@ engines:
- Groq
- 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.

**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
34 changes: 26 additions & 8 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 @@ -20,6 +20,7 @@ import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
import { AnswerSection } from '@/components/answer-section'
import { workflow } from '@/lib/actions/workflow'
import { getRedisClient } from '@/lib/redis/config'

const MAX_MESSAGES = 6

Expand All @@ -35,6 +36,11 @@ 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 @@ -70,8 +76,16 @@ async function submit(
? 'input_related'
: 'inquiry'

// Add the user message to the state
if (content) {
// 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) {
miurla marked this conversation as resolved.
Show resolved Hide resolved
aiState.update({
...aiState.get(),
messages: [
Expand All @@ -84,10 +98,6 @@ async function submit(
}
]
})
messages.push({
role: 'user',
content
})
}

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

const redis = await getRedisClient()
const chatHistoryEnabled = await redis.get(
Copy link
Owner

Choose a reason for hiding this comment

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

I tested with Upstash, and even when disabled, it was always saved. The reason was that chatHistoryEnabled was returning a boolean value. Please check this.

console.log({
  value: chatHistoryEnabled,
  type: typeof chatHistoryEnabled,
  comparison: chatHistoryEnabled === 'false',
  strictEquality: Object.is(chatHistoryEnabled, 'false')
})
{
  value: false,
  type: 'boolean',
  comparison: false,
  strictEquality: false
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm thats odd i tested with local redis and was working and upstash . ok will have a look

Copy link
Owner

Choose a reason for hiding this comment

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

I performed additional testing: #369 (comment)

'user:anonymous:chatHistoryEnabled'
)

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

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>
)
}
29 changes: 29 additions & 0 deletions components/chat-history-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'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 } = useChatHistory()

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}
/>
<Label htmlFor="chat-history">Enable Chat History</Label>
</div>
)
}
12 changes: 9 additions & 3 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { EmptyScreen } from './empty-screen'
import Textarea from 'react-textarea-autosize'
import { generateId } from 'ai'
import { useAppState } from '@/lib/utils/app-state'
import { useChatHistory } from '@/lib/utils/chat-history-context'

interface ChatPanelProps {
messages: UIState
query?: string
}

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

// Add user message to UI state
// Always add user message to UI state
setMessages(currentMessages => [
...currentMessages,
{
Expand All @@ -61,6 +63,8 @@ export function ChatPanel({ messages, query }: ChatPanelProps) {
data.append('input', query)
}
const responseMessage = await submit(data)

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

Expand Down Expand Up @@ -89,8 +93,10 @@ export function ChatPanel({ messages, query }: ChatPanelProps) {
// Clear messages
const handleClear = () => {
setIsGenerating(false)
setMessages([])
setAIMessage({ messages: [], chatId: '' })
if (chatHistoryEnabled) {
setMessages([])
setAIMessage({ messages: [], chatId: '' })
}
setInput('')
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