Merge pull request #340 from lfnovo/fix/chat-model-selection
fix: chat model selection issues + Docker troubleshooting docs
This commit is contained in:
commit
bbe06df7e4
7 changed files with 215 additions and 24 deletions
|
|
@ -8,9 +8,12 @@ Click your problem:
|
|||
|
||||
### 1. ["Unable to connect to server" or blank page](#fix-connection-error)
|
||||
### 2. [Container won't start or crashes](#fix-container-crash)
|
||||
### 3. [Works on server but not from my computer](#fix-remote-access)
|
||||
### 4. [API or authentication errors](#fix-api-errors)
|
||||
### 5. [Slow or timeout errors](#fix-performance)
|
||||
### 3. [Quotes in environment variables](#fix-quotes-in-env)
|
||||
### 4. [SurrealDB configuration issues](#fix-surrealdb-config)
|
||||
### 5. [Network timeouts (slow connections / China)](#fix-network-timeouts)
|
||||
### 6. [Works on server but not from my computer](#fix-remote-access)
|
||||
### 7. [API or authentication errors](#fix-api-errors)
|
||||
### 8. [Slow or timeout errors](#fix-performance)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -128,6 +131,9 @@ docker compose logs
|
|||
| "Invalid API key" | Check OPENAI_API_KEY in environment variables |
|
||||
| "Out of memory" | Increase Docker memory limit to 2GB+ in Docker Desktop settings |
|
||||
| "No such file or directory" | Check volume paths exist and are accessible |
|
||||
| "'' is not a valid UrlScheme" | [Remove quotes from environment variables](#fix-quotes-in-env) |
|
||||
| "There was a problem with authentication" | [Check SurrealDB configuration](#fix-surrealdb-config) |
|
||||
| Worker/API crashes on startup | [Check network timeouts](#fix-network-timeouts) |
|
||||
|
||||
**Quick reset:**
|
||||
```bash
|
||||
|
|
@ -135,6 +141,127 @@ docker compose down -v
|
|||
docker compose up -d
|
||||
```
|
||||
|
||||
<a name="fix-quotes-in-env"></a>
|
||||
### Fix: Quotes in Environment Variables
|
||||
|
||||
**Symptom:** Error `'' is not a valid UrlScheme` or database connection fails with empty URL.
|
||||
|
||||
**Cause:** Docker Compose interprets quotes literally. If you have quotes around values in your `docker-compose.yml` or `.env` file, they become part of the value.
|
||||
|
||||
❌ **Wrong** (quotes become part of the value):
|
||||
```yaml
|
||||
environment:
|
||||
- SURREAL_URL="ws://localhost:8000/rpc"
|
||||
- SURREAL_USER="root"
|
||||
```
|
||||
|
||||
❌ **Also wrong** in `.env` files:
|
||||
```env
|
||||
SURREAL_URL="ws://localhost:8000/rpc"
|
||||
SURREAL_USER="root"
|
||||
```
|
||||
|
||||
✅ **Correct** (no quotes):
|
||||
```yaml
|
||||
environment:
|
||||
- SURREAL_URL=ws://localhost:8000/rpc
|
||||
- SURREAL_USER=root
|
||||
- SURREAL_PASSWORD=root
|
||||
- SURREAL_NAMESPACE=open_notebook
|
||||
- SURREAL_DATABASE=production
|
||||
```
|
||||
|
||||
✅ **Correct** `.env` file:
|
||||
```env
|
||||
SURREAL_URL=ws://localhost:8000/rpc
|
||||
SURREAL_USER=root
|
||||
```
|
||||
|
||||
After fixing, restart:
|
||||
```bash
|
||||
docker compose down && docker compose up -d
|
||||
```
|
||||
|
||||
<a name="fix-surrealdb-config"></a>
|
||||
### Fix: SurrealDB Configuration Issues
|
||||
|
||||
#### Single Container Already Has SurrealDB
|
||||
|
||||
**Symptom:** Authentication errors or connection issues when using `v1-latest-single` with an external SurrealDB.
|
||||
|
||||
**Cause:** The `-single` image already includes SurrealDB. You don't need to run a separate SurrealDB container.
|
||||
|
||||
❌ **Wrong** - running separate SurrealDB with single container:
|
||||
```yaml
|
||||
services:
|
||||
surrealdb:
|
||||
image: surrealdb/surrealdb:latest # Not needed!
|
||||
|
||||
open_notebook:
|
||||
image: lfnovo/open_notebook:v1-latest-single
|
||||
environment:
|
||||
- SURREAL_URL=ws://surrealdb:8000/rpc # Wrong!
|
||||
```
|
||||
|
||||
✅ **Correct** - single container uses built-in SurrealDB:
|
||||
```yaml
|
||||
services:
|
||||
open_notebook:
|
||||
image: lfnovo/open_notebook:v1-latest-single
|
||||
environment:
|
||||
- SURREAL_URL=ws://localhost:8000/rpc # Uses internal DB
|
||||
```
|
||||
|
||||
**If you want a separate SurrealDB**, use the `v1-latest` image (without `-single`) instead.
|
||||
|
||||
#### SurrealDB Version Compatibility
|
||||
|
||||
**Symptom:** Various database errors, authentication failures, or unexpected behavior.
|
||||
|
||||
**Cause:** Open Notebook currently supports **SurrealDB v2.x only**. SurrealDB v3 (alpha) is not yet supported.
|
||||
|
||||
✅ **Supported versions:**
|
||||
```yaml
|
||||
# Use v2.x
|
||||
image: surrealdb/surrealdb:v2.1.4
|
||||
image: surrealdb/surrealdb:v2 # Latest v2
|
||||
```
|
||||
|
||||
❌ **Not supported yet:**
|
||||
```yaml
|
||||
# Don't use v3 alpha
|
||||
image: surrealdb/surrealdb:v3.0.0-alpha.17
|
||||
```
|
||||
|
||||
<a name="fix-network-timeouts"></a>
|
||||
### Fix: Network Timeouts (Slow Connections / China)
|
||||
|
||||
**Symptom:** Container crashes on startup with `exit status 1`, worker enters FATAL state, or pip/uv dependency downloads fail.
|
||||
|
||||
**Cause:** The container downloads Python dependencies on first startup. Slow networks or restricted access (especially in China) can cause timeouts.
|
||||
|
||||
✅ **Fix:** Add timeout and mirror configuration:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
open_notebook:
|
||||
image: lfnovo/open_notebook:v1-latest-single
|
||||
environment:
|
||||
# Increase download timeout to 10 minutes (default is 30s)
|
||||
- UV_HTTP_TIMEOUT=600
|
||||
|
||||
# For users in China - use mirror
|
||||
- UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
- PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
**Alternative mirrors for China:**
|
||||
- Tsinghua: `https://pypi.tuna.tsinghua.edu.cn/simple`
|
||||
- Aliyun: `https://mirrors.aliyun.com/pypi/simple/`
|
||||
- Huawei: `https://repo.huaweicloud.com/repository/pypi/simple`
|
||||
|
||||
**Note:** First startup may take several minutes while dependencies are downloaded. Subsequent startups will be faster.
|
||||
|
||||
---
|
||||
|
||||
<a name="fix-remote-access"></a>
|
||||
|
|
|
|||
|
|
@ -95,12 +95,8 @@ export function ChatColumn({ notebookId, contextSelections }: ChatColumnProps) {
|
|||
isStreaming={chat.isSending}
|
||||
contextIndicators={null}
|
||||
onSendMessage={(message, modelOverride) => chat.sendMessage(message, modelOverride)}
|
||||
modelOverride={chat.currentSession?.model_override ?? undefined}
|
||||
onModelChange={(model) => {
|
||||
if (chat.currentSessionId) {
|
||||
chat.updateSession(chat.currentSessionId, { model_override: model ?? null })
|
||||
}
|
||||
}}
|
||||
modelOverride={chat.currentSession?.model_override ?? chat.pendingModelOverride ?? undefined}
|
||||
onModelChange={(model) => chat.setModelOverride(model ?? null)}
|
||||
sessions={chat.sessions}
|
||||
currentSessionId={chat.currentSessionId}
|
||||
onCreateSession={(title) => chat.createSession(title)}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
MessageSquare,
|
||||
Plus,
|
||||
Trash2,
|
||||
Edit2,
|
||||
import {
|
||||
MessageSquare,
|
||||
Plus,
|
||||
Trash2,
|
||||
Edit2,
|
||||
Check,
|
||||
X,
|
||||
Clock
|
||||
|
|
@ -27,6 +27,7 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { BaseChatSession } from '@/lib/types/api'
|
||||
import { useModels } from '@/lib/hooks/use-models'
|
||||
|
||||
interface SessionManagerProps {
|
||||
sessions: BaseChatSession[]
|
||||
|
|
@ -53,6 +54,16 @@ export function SessionManager({
|
|||
const [editTitle, setEditTitle] = useState('')
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
|
||||
|
||||
const { data: models } = useModels()
|
||||
|
||||
// Helper to get model name from ID
|
||||
const getModelName = useMemo(() => {
|
||||
return (modelId: string) => {
|
||||
const model = models?.find(m => m.id === modelId)
|
||||
return model?.name || 'Custom Model'
|
||||
}
|
||||
}, [models])
|
||||
|
||||
const handleCreateSession = () => {
|
||||
if (newSessionTitle.trim()) {
|
||||
onCreateSession(newSessionTitle.trim())
|
||||
|
|
@ -211,14 +222,14 @@ export function SessionManager({
|
|||
<Clock className="h-3 w-3" />
|
||||
{formatDistanceToNow(new Date(session.created), { addSuffix: true })}
|
||||
</div>
|
||||
{session.message_count && session.message_count > 0 && (
|
||||
{session.message_count != null && session.message_count > 0 && (
|
||||
<Badge variant="secondary" className="mt-2 text-xs">
|
||||
{session.message_count} messages
|
||||
</Badge>
|
||||
)}
|
||||
{session.model_override && (
|
||||
<Badge variant="outline" className="mt-2 ml-2 text-xs">
|
||||
{session.model_override}
|
||||
{getModelName(session.model_override)}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { FileText } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { useInsight } from '@/lib/hooks/use-insights'
|
||||
import { useModalManager } from '@/lib/hooks/use-modal-manager'
|
||||
|
||||
interface SourceInsightDialogProps {
|
||||
open: boolean
|
||||
|
|
@ -14,10 +17,13 @@ interface SourceInsightDialogProps {
|
|||
insight_type?: string
|
||||
content?: string
|
||||
created?: string
|
||||
source_id?: string
|
||||
}
|
||||
}
|
||||
|
||||
export function SourceInsightDialog({ open, onOpenChange, insight }: SourceInsightDialogProps) {
|
||||
const { openModal } = useModalManager()
|
||||
|
||||
// Ensure insight ID has 'source_insight:' prefix for API calls
|
||||
const insightIdWithPrefix = insight?.id
|
||||
? (insight.id.includes(':') ? insight.id : `source_insight:${insight.id}`)
|
||||
|
|
@ -28,17 +34,39 @@ export function SourceInsightDialog({ open, onOpenChange, insight }: SourceInsig
|
|||
// Use fetched data if available, otherwise fall back to passed-in insight
|
||||
const displayInsight = fetchedInsight ?? insight
|
||||
|
||||
// Get source_id from fetched data (preferred) or passed-in insight
|
||||
const sourceId = fetchedInsight?.source_id ?? insight?.source_id
|
||||
|
||||
const handleViewSource = () => {
|
||||
if (sourceId) {
|
||||
openModal('source', sourceId)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-3xl max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-between gap-2">
|
||||
<span>Source Insight</span>
|
||||
{displayInsight?.insight_type && (
|
||||
<Badge variant="outline" className="text-xs uppercase">
|
||||
{displayInsight.insight_type}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{displayInsight?.insight_type && (
|
||||
<Badge variant="outline" className="text-xs uppercase">
|
||||
{displayInsight.insight_type}
|
||||
</Badge>
|
||||
)}
|
||||
{sourceId && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleViewSource}
|
||||
className="gap-1"
|
||||
>
|
||||
<FileText className="h-3 w-3" />
|
||||
View Source
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
|||
const [isSending, setIsSending] = useState(false)
|
||||
const [tokenCount, setTokenCount] = useState<number>(0)
|
||||
const [charCount, setCharCount] = useState<number>(0)
|
||||
// Pending model override for when user changes model before a session exists
|
||||
const [pendingModelOverride, setPendingModelOverride] = useState<string | null>(null)
|
||||
|
||||
// Fetch sessions for this notebook
|
||||
const {
|
||||
|
|
@ -176,10 +178,14 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
|||
: message
|
||||
const newSession = await chatApi.createSession({
|
||||
notebook_id: notebookId,
|
||||
title: defaultTitle
|
||||
title: defaultTitle,
|
||||
// Include pending model override when creating session
|
||||
model_override: pendingModelOverride ?? undefined
|
||||
})
|
||||
sessionId = newSession.id
|
||||
setCurrentSessionId(sessionId)
|
||||
// Clear pending model override now that it's applied to the session
|
||||
setPendingModelOverride(null)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: QUERY_KEYS.notebookChatSessions(notebookId)
|
||||
})
|
||||
|
|
@ -226,6 +232,7 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
|||
notebookId,
|
||||
currentSessionId,
|
||||
currentSession,
|
||||
pendingModelOverride,
|
||||
buildContext,
|
||||
refetchCurrentSession,
|
||||
queryClient
|
||||
|
|
@ -257,6 +264,20 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
|||
return deleteSessionMutation.mutate(sessionId)
|
||||
}, [deleteSessionMutation])
|
||||
|
||||
// Set model override - handles both existing sessions and pending state
|
||||
const setModelOverride = useCallback((model: string | null) => {
|
||||
if (currentSessionId) {
|
||||
// Session exists - update it directly
|
||||
updateSessionMutation.mutate({
|
||||
sessionId: currentSessionId,
|
||||
data: { model_override: model }
|
||||
})
|
||||
} else {
|
||||
// No session yet - store as pending
|
||||
setPendingModelOverride(model)
|
||||
}
|
||||
}, [currentSessionId, updateSessionMutation])
|
||||
|
||||
// Update token/char counts when context selections change
|
||||
useEffect(() => {
|
||||
const updateContextCounts = async () => {
|
||||
|
|
@ -279,6 +300,7 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
|||
loadingSessions,
|
||||
tokenCount,
|
||||
charCount,
|
||||
pendingModelOverride,
|
||||
|
||||
// Actions
|
||||
createSession,
|
||||
|
|
@ -286,6 +308,7 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections
|
|||
deleteSession,
|
||||
switchSession,
|
||||
sendMessage,
|
||||
setModelOverride,
|
||||
refetchSessions
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ T = TypeVar("T", bound="ObjectModel")
|
|||
class ObjectModel(BaseModel):
|
||||
id: Optional[str] = None
|
||||
table_name: ClassVar[str] = ""
|
||||
nullable_fields: ClassVar[set[str]] = set() # Fields that can be saved as None
|
||||
created: Optional[datetime] = None
|
||||
updated: Optional[datetime] = None
|
||||
|
||||
|
|
@ -167,7 +168,11 @@ class ObjectModel(BaseModel):
|
|||
|
||||
def _prepare_save_data(self) -> Dict[str, Any]:
|
||||
data = self.model_dump()
|
||||
return {key: value for key, value in data.items() if value is not None}
|
||||
return {
|
||||
key: value
|
||||
for key, value in data.items()
|
||||
if value is not None or key in self.__class__.nullable_fields
|
||||
}
|
||||
|
||||
async def delete(self) -> bool:
|
||||
if self.id is None:
|
||||
|
|
|
|||
|
|
@ -389,6 +389,7 @@ class Note(ObjectModel):
|
|||
|
||||
class ChatSession(ObjectModel):
|
||||
table_name: ClassVar[str] = "chat_session"
|
||||
nullable_fields: ClassVar[set[str]] = {"model_override"}
|
||||
title: Optional[str] = None
|
||||
model_override: Optional[str] = None
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue