From 33b8f7a2b8752c3a20f0d5c714f2496f3d8478d5 Mon Sep 17 00:00:00 2001 From: LUIS NOVO Date: Fri, 19 Dec 2025 16:30:55 -0300 Subject: [PATCH 1/4] docs: add troubleshooting for common Docker installation issues - Add fix for quotes in environment variables causing empty URL errors - Add SurrealDB configuration section (single container includes DB, v2 only) - Add network timeout fix for slow connections and Chinese users - Update table of contents with new sections Addresses feedback from issue #316 --- docs/troubleshooting/quick-fixes.md | 133 +++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 3 deletions(-) diff --git a/docs/troubleshooting/quick-fixes.md b/docs/troubleshooting/quick-fixes.md index a829292..f5ca571 100644 --- a/docs/troubleshooting/quick-fixes.md +++ b/docs/troubleshooting/quick-fixes.md @@ -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 ``` + +### 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 +``` + + +### 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 +``` + + +### 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. + --- From e11f0a4db876bc08a85a803c3d99a7ae98138877 Mon Sep 17 00:00:00 2001 From: LUIS NOVO Date: Fri, 19 Dec 2025 16:47:34 -0300 Subject: [PATCH 2/4] fix: resolve chat model selection and session display issues - Add nullable_fields support to ObjectModel base class - Configure ChatSession to allow model_override to be cleared to null - Fix JSX conditional that rendered "0" when message_count was 0 - Display model name instead of raw ID in session manager Fixes issues: 1. Switching to default model now persists correctly 2. Session list shows human-readable model names 3. Sessions with 0 messages no longer show "0" badge --- .../src/components/source/SessionManager.tsx | 27 +++++++++++++------ open_notebook/domain/base.py | 7 ++++- open_notebook/domain/notebook.py | 1 + 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/source/SessionManager.tsx b/frontend/src/components/source/SessionManager.tsx index cec74fa..001a42b 100644 --- a/frontend/src/components/source/SessionManager.tsx +++ b/frontend/src/components/source/SessionManager.tsx @@ -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(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({ {formatDistanceToNow(new Date(session.created), { addSuffix: true })} - {session.message_count && session.message_count > 0 && ( + {session.message_count != null && session.message_count > 0 && ( {session.message_count} messages )} {session.model_override && ( - {session.model_override} + {getModelName(session.model_override)} )} diff --git a/open_notebook/domain/base.py b/open_notebook/domain/base.py index 83c1b3d..f9992e2 100644 --- a/open_notebook/domain/base.py +++ b/open_notebook/domain/base.py @@ -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: diff --git a/open_notebook/domain/notebook.py b/open_notebook/domain/notebook.py index cf096bd..2f589a6 100644 --- a/open_notebook/domain/notebook.py +++ b/open_notebook/domain/notebook.py @@ -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 From a1d0a3a666a4e1146e9943a40a489c7579d6216d Mon Sep 17 00:00:00 2001 From: LUIS NOVO Date: Fri, 19 Dec 2025 16:55:26 -0300 Subject: [PATCH 3/4] fix: allow model override before chat session exists - Add pendingModelOverride state to useNotebookChat hook - Store model selection when no session exists yet - Apply pending model override when session is auto-created on first message - Simplify ChatColumn by using new setModelOverride function --- .../notebooks/components/ChatColumn.tsx | 8 ++---- frontend/src/lib/hooks/useNotebookChat.ts | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx b/frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx index b65d4a3..be8ce32 100644 --- a/frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx +++ b/frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx @@ -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)} diff --git a/frontend/src/lib/hooks/useNotebookChat.ts b/frontend/src/lib/hooks/useNotebookChat.ts index c10249c..e97cd18 100644 --- a/frontend/src/lib/hooks/useNotebookChat.ts +++ b/frontend/src/lib/hooks/useNotebookChat.ts @@ -28,6 +28,8 @@ export function useNotebookChat({ notebookId, sources, notes, contextSelections const [isSending, setIsSending] = useState(false) const [tokenCount, setTokenCount] = useState(0) const [charCount, setCharCount] = useState(0) + // Pending model override for when user changes model before a session exists + const [pendingModelOverride, setPendingModelOverride] = useState(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 } } From 7f19c916ae978e035cc95520650572508323ea0e Mon Sep 17 00:00:00 2001 From: LUIS NOVO Date: Fri, 19 Dec 2025 17:10:55 -0300 Subject: [PATCH 4/4] feat: add View Source button to insight modal - Add button to navigate from insight popup to its parent source - Uses existing modal manager to open source detail view - Shows button only when source_id is available --- .../components/source/SourceInsightDialog.tsx | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/source/SourceInsightDialog.tsx b/frontend/src/components/source/SourceInsightDialog.tsx index 6103e7d..9946277 100644 --- a/frontend/src/components/source/SourceInsightDialog.tsx +++ b/frontend/src/components/source/SourceInsightDialog.tsx @@ -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 ( Source Insight - {displayInsight?.insight_type && ( - - {displayInsight.insight_type} - - )} +
+ {displayInsight?.insight_type && ( + + {displayInsight.insight_type} + + )} + {sourceId && ( + + )} +