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 4 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 '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 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
46 changes: 46 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,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 @@ -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 Down Expand Up @@ -70,7 +71,7 @@ async function submit(
? 'input_related'
: 'inquiry'

// Add the user message to the state
// Always update AIState if content exists
if (content) {
aiState.update({
...aiState.get(),
Expand Down Expand Up @@ -147,11 +148,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 @@ -161,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
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>
)
}
13 changes: 9 additions & 4 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,9 +93,10 @@ export function ChatPanel({ messages, query }: 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 Expand Up @@ -143,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
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} />
</>
)
}
Loading