Merge pull request #325 from lfnovo/feat/ui-improv
feat: UI improvements and bug fixes
This commit is contained in:
commit
040e7ccf42
20 changed files with 821 additions and 376 deletions
|
|
@ -76,6 +76,10 @@ EXPOSE 8502 5055
|
||||||
|
|
||||||
RUN mkdir -p /app/data
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# Copy and make executable the wait-for-api script
|
||||||
|
COPY scripts/wait-for-api.sh /app/scripts/wait-for-api.sh
|
||||||
|
RUN chmod +x /app/scripts/wait-for-api.sh
|
||||||
|
|
||||||
# Copy supervisord configuration
|
# Copy supervisord configuration
|
||||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,10 @@ COPY --from=builder /app/frontend/public /app/frontend/public
|
||||||
# Create directories for data persistence
|
# Create directories for data persistence
|
||||||
RUN mkdir -p /app/data /mydata
|
RUN mkdir -p /app/data /mydata
|
||||||
|
|
||||||
|
# Copy and make executable the wait-for-api script
|
||||||
|
COPY scripts/wait-for-api.sh /app/scripts/wait-for-api.sh
|
||||||
|
RUN chmod +x /app/scripts/wait-for-api.sh
|
||||||
|
|
||||||
# Expose ports for Frontend and API
|
# Expose ports for Frontend and API
|
||||||
EXPOSE 8502 5055
|
EXPOSE 8502 5055
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,9 @@ class ChatService:
|
||||||
if model_override is not None:
|
if model_override is not None:
|
||||||
data["model_override"] = model_override
|
data["model_override"] = model_override
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=120.0) as client: # Longer timeout for chat
|
# Short connect timeout (10s), long read timeout (10 min) for Ollama/local LLMs
|
||||||
|
timeout = httpx.Timeout(connect=10.0, read=600.0, write=30.0, pool=10.0)
|
||||||
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"{self.base_url}/api/chat/execute",
|
f"{self.base_url}/api/chat/execute",
|
||||||
json=data,
|
json=data,
|
||||||
|
|
|
||||||
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
|
|
@ -41,6 +41,7 @@
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.60.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"use-debounce": "^10.0.6",
|
"use-debounce": "^10.0.6",
|
||||||
|
|
@ -2748,7 +2749,6 @@
|
||||||
"version": "19.1.8",
|
"version": "19.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
|
|
@ -2758,7 +2758,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
|
||||||
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
|
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2811,7 +2810,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
|
||||||
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
|
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.37.0",
|
"@typescript-eslint/scope-manager": "8.37.0",
|
||||||
"@typescript-eslint/types": "8.37.0",
|
"@typescript-eslint/types": "8.37.0",
|
||||||
|
|
@ -3393,7 +3391,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -4454,7 +4451,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
||||||
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -4622,7 +4618,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
|
|
@ -7935,7 +7930,6 @@
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -7944,7 +7938,6 @@
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
|
|
@ -7956,7 +7949,6 @@
|
||||||
"version": "7.60.0",
|
"version": "7.60.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz",
|
||||||
"integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==",
|
"integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -8327,6 +8319,7 @@
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
||||||
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
|
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/mdast": "^4.0.0",
|
"@types/mdast": "^4.0.0",
|
||||||
"mdast-util-gfm": "^3.0.0",
|
"mdast-util-gfm": "^3.0.0",
|
||||||
|
|
@ -8994,8 +8987,7 @@
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
||||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
|
|
@ -9058,7 +9050,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -9225,7 +9216,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.60.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.6",
|
"sonner": "^2.0.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"use-debounce": "^10.0.6",
|
"use-debounce": "^10.0.6",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { SourcesColumn } from '../components/SourcesColumn'
|
||||||
import { NotesColumn } from '../components/NotesColumn'
|
import { NotesColumn } from '../components/NotesColumn'
|
||||||
import { ChatColumn } from '../components/ChatColumn'
|
import { ChatColumn } from '../components/ChatColumn'
|
||||||
import { useNotebook } from '@/lib/hooks/use-notebooks'
|
import { useNotebook } from '@/lib/hooks/use-notebooks'
|
||||||
import { useSources } from '@/lib/hooks/use-sources'
|
import { useNotebookSources } from '@/lib/hooks/use-sources'
|
||||||
import { useNotes } from '@/lib/hooks/use-notes'
|
import { useNotes } from '@/lib/hooks/use-notes'
|
||||||
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
|
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
|
||||||
import { useNotebookColumnsStore } from '@/lib/stores/notebook-columns-store'
|
import { useNotebookColumnsStore } from '@/lib/stores/notebook-columns-store'
|
||||||
|
|
@ -31,7 +31,14 @@ export default function NotebookPage() {
|
||||||
const notebookId = decodeURIComponent(params.id as string)
|
const notebookId = decodeURIComponent(params.id as string)
|
||||||
|
|
||||||
const { data: notebook, isLoading: notebookLoading } = useNotebook(notebookId)
|
const { data: notebook, isLoading: notebookLoading } = useNotebook(notebookId)
|
||||||
const { data: sources, isLoading: sourcesLoading, refetch: refetchSources } = useSources(notebookId)
|
const {
|
||||||
|
sources,
|
||||||
|
isLoading: sourcesLoading,
|
||||||
|
refetch: refetchSources,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
} = useNotebookSources(notebookId)
|
||||||
const { data: notes, isLoading: notesLoading } = useNotes(notebookId)
|
const { data: notes, isLoading: notesLoading } = useNotes(notebookId)
|
||||||
|
|
||||||
// Get collapse states for dynamic layout
|
// Get collapse states for dynamic layout
|
||||||
|
|
@ -153,6 +160,9 @@ export default function NotebookPage() {
|
||||||
onRefresh={refetchSources}
|
onRefresh={refetchSources}
|
||||||
contextSelections={contextSelections.sources}
|
contextSelections={contextSelections.sources}
|
||||||
onContextModeChange={(sourceId, mode) => handleContextModeChange(sourceId, mode, 'source')}
|
onContextModeChange={(sourceId, mode) => handleContextModeChange(sourceId, mode, 'source')}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
fetchNextPage={fetchNextPage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mobileActiveTab === 'notes' && (
|
{mobileActiveTab === 'notes' && (
|
||||||
|
|
@ -192,6 +202,9 @@ export default function NotebookPage() {
|
||||||
onRefresh={refetchSources}
|
onRefresh={refetchSources}
|
||||||
contextSelections={contextSelections.sources}
|
contextSelections={contextSelections.sources}
|
||||||
onContextModeChange={(sourceId, mode) => handleContextModeChange(sourceId, mode, 'source')}
|
onContextModeChange={(sourceId, mode) => handleContextModeChange(sourceId, mode, 'source')}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
fetchNextPage={fetchNextPage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo, useRef, useCallback, useEffect } from 'react'
|
||||||
import { SourceListResponse } from '@/lib/types/api'
|
import { SourceListResponse } from '@/lib/types/api'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { Plus, FileText, Link2, ChevronDown } from 'lucide-react'
|
import { Plus, FileText, Link2, ChevronDown, Loader2 } from 'lucide-react'
|
||||||
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
|
import { LoadingSpinner } from '@/components/common/LoadingSpinner'
|
||||||
import { EmptyState } from '@/components/common/EmptyState'
|
import { EmptyState } from '@/components/common/EmptyState'
|
||||||
import { AddSourceDialog } from '@/components/sources/AddSourceDialog'
|
import { AddSourceDialog } from '@/components/sources/AddSourceDialog'
|
||||||
|
|
@ -31,6 +31,10 @@ interface SourcesColumnProps {
|
||||||
onRefresh?: () => void
|
onRefresh?: () => void
|
||||||
contextSelections?: Record<string, ContextMode>
|
contextSelections?: Record<string, ContextMode>
|
||||||
onContextModeChange?: (sourceId: string, mode: ContextMode) => void
|
onContextModeChange?: (sourceId: string, mode: ContextMode) => void
|
||||||
|
// Pagination props
|
||||||
|
hasNextPage?: boolean
|
||||||
|
isFetchingNextPage?: boolean
|
||||||
|
fetchNextPage?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SourcesColumn({
|
export function SourcesColumn({
|
||||||
|
|
@ -39,7 +43,10 @@ export function SourcesColumn({
|
||||||
notebookId,
|
notebookId,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
contextSelections,
|
contextSelections,
|
||||||
onContextModeChange
|
onContextModeChange,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
}: SourcesColumnProps) {
|
}: SourcesColumnProps) {
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||||
const [addDialogOpen, setAddDialogOpen] = useState(false)
|
const [addDialogOpen, setAddDialogOpen] = useState(false)
|
||||||
|
|
@ -60,6 +67,30 @@ export function SourcesColumn({
|
||||||
() => createCollapseButton(toggleSources, 'Sources'),
|
() => createCollapseButton(toggleSources, 'Sources'),
|
||||||
[toggleSources]
|
[toggleSources]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Scroll container ref for infinite scroll
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Handle scroll for infinite loading
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
const container = scrollContainerRef.current
|
||||||
|
if (!container || !hasNextPage || isFetchingNextPage || !fetchNextPage) return
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = container
|
||||||
|
// Load more when user scrolls within 200px of the bottom
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 200) {
|
||||||
|
fetchNextPage()
|
||||||
|
}
|
||||||
|
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
|
||||||
|
|
||||||
|
// Attach scroll listener
|
||||||
|
useEffect(() => {
|
||||||
|
const container = scrollContainerRef.current
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
container.addEventListener('scroll', handleScroll)
|
||||||
|
return () => container.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [handleScroll])
|
||||||
|
|
||||||
const handleDeleteClick = (sourceId: string) => {
|
const handleDeleteClick = (sourceId: string) => {
|
||||||
setSourceToDelete(sourceId)
|
setSourceToDelete(sourceId)
|
||||||
|
|
@ -149,7 +180,7 @@ export function SourcesColumn({
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex-1 overflow-y-auto min-h-0">
|
<CardContent ref={scrollContainerRef} className="flex-1 overflow-y-auto min-h-0">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
|
|
@ -179,6 +210,12 @@ export function SourcesColumn({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{/* Loading indicator for infinite scroll */}
|
||||||
|
{isFetchingNextPage && (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { Transformation } from '@/lib/types/transformations'
|
||||||
import { useExecuteTransformation } from '@/lib/hooks/use-transformations'
|
import { useExecuteTransformation } from '@/lib/hooks/use-transformations'
|
||||||
import { ModelSelector } from '@/components/common/ModelSelector'
|
import { ModelSelector } from '@/components/common/ModelSelector'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
|
|
||||||
interface TransformationPlaygroundProps {
|
interface TransformationPlaygroundProps {
|
||||||
transformations: Transformation[] | undefined
|
transformations: Transformation[] | undefined
|
||||||
|
|
@ -118,8 +119,24 @@ export function TransformationPlayground({ transformations, selectedTransformati
|
||||||
<Card>
|
<Card>
|
||||||
<ScrollArea className="h-[400px]">
|
<ScrollArea className="h-[400px]">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="prose prose-sm max-w-none">
|
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||||
<ReactMarkdown>{output}</ReactMarkdown>
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
table: ({ children }) => (
|
||||||
|
<div className="my-4 overflow-x-auto">
|
||||||
|
<table className="min-w-full border-collapse border border-border">{children}</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ children }) => <thead className="bg-muted">{children}</thead>,
|
||||||
|
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||||
|
tr: ({ children }) => <tr className="border-b border-border">{children}</tr>,
|
||||||
|
th: ({ children }) => <th className="border border-border px-3 py-2 text-left font-semibold">{children}</th>,
|
||||||
|
td: ({ children }) => <td className="border border-border px-3 py-2">{children}</td>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{output}
|
||||||
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
|
||||||
import { CheckCircle, Sparkles, Lightbulb, ChevronDown } from 'lucide-react'
|
import { CheckCircle, Sparkles, Lightbulb, ChevronDown } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
import { convertReferencesToMarkdownLinks, createReferenceLinkComponent } from '@/lib/utils/source-references'
|
import { convertReferencesToMarkdownLinks, createReferenceLinkComponent } from '@/lib/utils/source-references'
|
||||||
import { useModalManager } from '@/lib/hooks/use-modal-manager'
|
import { useModalManager } from '@/lib/hooks/use-modal-manager'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
@ -172,8 +173,19 @@ function FinalAnswerContent({
|
||||||
return (
|
return (
|
||||||
<div className="prose prose-sm max-w-none dark:prose-invert break-words prose-a:break-all prose-p:leading-relaxed prose-headings:mt-4 prose-headings:mb-2">
|
<div className="prose prose-sm max-w-none dark:prose-invert break-words prose-a:break-all prose-p:leading-relaxed prose-headings:mt-4 prose-headings:mb-2">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
components={{
|
||||||
a: LinkComponent
|
a: LinkComponent,
|
||||||
|
table: ({ children }) => (
|
||||||
|
<div className="my-4 overflow-x-auto">
|
||||||
|
<table className="min-w-full border-collapse border border-border">{children}</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ children }) => <thead className="bg-muted">{children}</thead>,
|
||||||
|
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||||
|
tr: ({ children }) => <tr className="border-b border-border">{children}</tr>,
|
||||||
|
th: ({ children }) => <th className="border border-border px-3 py-2 text-left font-semibold">{children}</th>,
|
||||||
|
td: ({ children }) => <td className="border border-border px-3 py-2">{children}</td>,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{markdownWithLinks}
|
{markdownWithLinks}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge'
|
||||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Bot, User, Send, Loader2, FileText, Lightbulb, StickyNote, Clock } from 'lucide-react'
|
import { Bot, User, Send, Loader2, FileText, Lightbulb, StickyNote, Clock } from 'lucide-react'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
import {
|
import {
|
||||||
SourceChatMessage,
|
SourceChatMessage,
|
||||||
SourceChatContextIndicator,
|
SourceChatContextIndicator,
|
||||||
|
|
@ -335,6 +336,7 @@ function AIMessageContent({
|
||||||
return (
|
return (
|
||||||
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none break-words prose-headings:font-semibold prose-a:text-blue-600 prose-a:break-all prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-p:mb-4 prose-p:leading-7 prose-li:mb-2">
|
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none break-words prose-headings:font-semibold prose-a:text-blue-600 prose-a:break-all prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-p:mb-4 prose-p:leading-7 prose-li:mb-2">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
components={{
|
||||||
a: LinkComponent,
|
a: LinkComponent,
|
||||||
p: ({ children }) => <p className="mb-4">{children}</p>,
|
p: ({ children }) => <p className="mb-4">{children}</p>,
|
||||||
|
|
@ -347,6 +349,16 @@ function AIMessageContent({
|
||||||
li: ({ children }) => <li className="mb-1">{children}</li>,
|
li: ({ children }) => <li className="mb-1">{children}</li>,
|
||||||
ul: ({ children }) => <ul className="mb-4 space-y-1">{children}</ul>,
|
ul: ({ children }) => <ul className="mb-4 space-y-1">{children}</ul>,
|
||||||
ol: ({ children }) => <ol className="mb-4 space-y-1">{children}</ol>,
|
ol: ({ children }) => <ol className="mb-4 space-y-1">{children}</ol>,
|
||||||
|
table: ({ children }) => (
|
||||||
|
<div className="my-4 overflow-x-auto">
|
||||||
|
<table className="min-w-full border-collapse border border-border">{children}</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ children }) => <thead className="bg-muted">{children}</thead>,
|
||||||
|
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||||
|
tr: ({ children }) => <tr className="border-b border-border">{children}</tr>,
|
||||||
|
th: ({ children }) => <th className="border border-border px-3 py-2 text-left font-semibold">{children}</th>,
|
||||||
|
td: ({ children }) => <td className="border border-border px-3 py-2">{children}</td>,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{markdownWithCompactRefs}
|
{markdownWithCompactRefs}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
import { isAxiosError } from 'axios'
|
import { isAxiosError } from 'axios'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
import { sourcesApi } from '@/lib/api/sources'
|
import { sourcesApi } from '@/lib/api/sources'
|
||||||
import { insightsApi, SourceInsightResponse } from '@/lib/api/insights'
|
import { insightsApi, SourceInsightResponse } from '@/lib/api/insights'
|
||||||
import { transformationsApi } from '@/lib/api/transformations'
|
import { transformationsApi } from '@/lib/api/transformations'
|
||||||
|
|
@ -461,6 +462,7 @@ export function SourceDetailContent({
|
||||||
)}
|
)}
|
||||||
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none prose-headings:font-semibold prose-a:text-blue-600 prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-p:mb-4 prose-p:leading-7 prose-li:mb-2">
|
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none prose-headings:font-semibold prose-a:text-blue-600 prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-p:mb-4 prose-p:leading-7 prose-li:mb-2">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
components={{
|
||||||
p: ({ children }) => <p className="mb-4">{children}</p>,
|
p: ({ children }) => <p className="mb-4">{children}</p>,
|
||||||
h1: ({ children }) => <h1 className="text-2xl font-bold mt-6 mb-4">{children}</h1>,
|
h1: ({ children }) => <h1 className="text-2xl font-bold mt-6 mb-4">{children}</h1>,
|
||||||
|
|
@ -469,6 +471,16 @@ export function SourceDetailContent({
|
||||||
ul: ({ children }) => <ul className="mb-4 list-disc pl-6">{children}</ul>,
|
ul: ({ children }) => <ul className="mb-4 list-disc pl-6">{children}</ul>,
|
||||||
ol: ({ children }) => <ol className="mb-4 list-decimal pl-6">{children}</ol>,
|
ol: ({ children }) => <ol className="mb-4 list-decimal pl-6">{children}</ol>,
|
||||||
li: ({ children }) => <li className="mb-1">{children}</li>,
|
li: ({ children }) => <li className="mb-1">{children}</li>,
|
||||||
|
table: ({ children }) => (
|
||||||
|
<div className="my-4 overflow-x-auto">
|
||||||
|
<table className="min-w-full border-collapse border border-border">{children}</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ children }) => <thead className="bg-muted">{children}</thead>,
|
||||||
|
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||||
|
tr: ({ children }) => <tr className="border-b border-border">{children}</tr>,
|
||||||
|
th: ({ children }) => <th className="border border-border px-3 py-2 text-left font-semibold">{children}</th>,
|
||||||
|
td: ({ children }) => <td className="border border-border px-3 py-2">{children}</td>,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{source.full_text || 'No content available'}
|
{source.full_text || 'No content available'}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
import { useInsight } from '@/lib/hooks/use-insights'
|
import { useInsight } from '@/lib/hooks/use-insights'
|
||||||
|
|
||||||
interface SourceInsightDialogProps {
|
interface SourceInsightDialogProps {
|
||||||
|
|
@ -48,7 +49,23 @@ export function SourceInsightDialog({ open, onOpenChange, insight }: SourceInsig
|
||||||
</div>
|
</div>
|
||||||
) : displayInsight ? (
|
) : displayInsight ? (
|
||||||
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none">
|
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none">
|
||||||
<ReactMarkdown>{displayInsight.content}</ReactMarkdown>
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
table: ({ children }) => (
|
||||||
|
<div className="my-4 overflow-x-auto">
|
||||||
|
<table className="min-w-full border-collapse border border-border">{children}</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ children }) => <thead className="bg-muted">{children}</thead>,
|
||||||
|
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||||
|
tr: ({ children }) => <tr className="border-b border-border">{children}</tr>,
|
||||||
|
th: ({ children }) => <th className="border border-border px-3 py-2 text-left font-semibold">{children}</th>,
|
||||||
|
td: ({ children }) => <td className="border border-border px-3 py-2">{children}</td>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayInsight.content}
|
||||||
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-muted-foreground">No insight selected.</p>
|
<p className="text-sm text-muted-foreground">No insight selected.</p>
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import { getApiUrl } from '@/lib/config'
|
||||||
|
|
||||||
// API client with runtime-configurable base URL
|
// API client with runtime-configurable base URL
|
||||||
// The base URL is fetched from the API config endpoint on first request
|
// The base URL is fetched from the API config endpoint on first request
|
||||||
// Timeout increased to 5 minutes (300000ms = 300s) to accommodate slow LLM operations
|
// Timeout increased to 10 minutes (600000ms = 600s) to accommodate slow LLM operations
|
||||||
// (transformations, insights generation) especially on slower hardware (Ollama, LM Studio)
|
// (transformations, insights generation, chat) especially on slower hardware (Ollama, LM Studio)
|
||||||
// Note: Frontend uses milliseconds (300000ms), backend uses seconds (300s) - both equal 5 minutes
|
// Note: Frontend uses milliseconds, backend uses seconds
|
||||||
// To configure: Set API_CLIENT_TIMEOUT=600 in .env for 10 minutes (600s = 600000ms)
|
// Local LLMs can take several minutes for complex questions with large contexts
|
||||||
export const apiClient = axios.create({
|
export const apiClient = axios.create({
|
||||||
timeout: 300000, // 300 seconds = 5 minutes
|
timeout: 600000, // 600 seconds = 10 minutes
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export const QUERY_KEYS = {
|
||||||
notes: (notebookId?: string) => ['notes', notebookId] as const,
|
notes: (notebookId?: string) => ['notes', notebookId] as const,
|
||||||
note: (id: string) => ['notes', id] as const,
|
note: (id: string) => ['notes', id] as const,
|
||||||
sources: (notebookId?: string) => ['sources', notebookId] as const,
|
sources: (notebookId?: string) => ['sources', notebookId] as const,
|
||||||
|
sourcesInfinite: (notebookId: string) => ['sources', 'infinite', notebookId] as const,
|
||||||
source: (id: string) => ['sources', id] as const,
|
source: (id: string) => ['sources', id] as const,
|
||||||
settings: ['settings'] as const,
|
settings: ['settings'] as const,
|
||||||
sourceChatSessions: (sourceId: string) => ['source-chat', sourceId, 'sessions'] as const,
|
sourceChatSessions: (sourceId: string) => ['source-chat', sourceId, 'sessions'] as const,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
import { sourcesApi } from '@/lib/api/sources'
|
import { sourcesApi } from '@/lib/api/sources'
|
||||||
import { QUERY_KEYS } from '@/lib/api/query-client'
|
import { QUERY_KEYS } from '@/lib/api/query-client'
|
||||||
import { useToast } from '@/lib/hooks/use-toast'
|
import { useToast } from '@/lib/hooks/use-toast'
|
||||||
import {
|
import {
|
||||||
CreateSourceRequest,
|
CreateSourceRequest,
|
||||||
UpdateSourceRequest,
|
UpdateSourceRequest,
|
||||||
SourceResponse,
|
SourceResponse,
|
||||||
SourceStatusResponse
|
SourceStatusResponse,
|
||||||
|
SourceListResponse
|
||||||
} from '@/lib/types/api'
|
} from '@/lib/types/api'
|
||||||
|
|
||||||
|
const NOTEBOOK_SOURCES_PAGE_SIZE = 30
|
||||||
|
|
||||||
export function useSources(notebookId?: string) {
|
export function useSources(notebookId?: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: QUERY_KEYS.sources(notebookId),
|
queryKey: QUERY_KEYS.sources(notebookId),
|
||||||
|
|
@ -19,6 +23,57 @@ export function useSources(notebookId?: string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for fetching notebook sources with infinite scroll pagination.
|
||||||
|
* Returns flattened sources array and pagination controls.
|
||||||
|
*/
|
||||||
|
export function useNotebookSources(notebookId: string) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const query = useInfiniteQuery({
|
||||||
|
queryKey: QUERY_KEYS.sourcesInfinite(notebookId),
|
||||||
|
queryFn: async ({ pageParam = 0 }) => {
|
||||||
|
const data = await sourcesApi.list({
|
||||||
|
notebook_id: notebookId,
|
||||||
|
limit: NOTEBOOK_SOURCES_PAGE_SIZE,
|
||||||
|
offset: pageParam,
|
||||||
|
sort_by: 'updated',
|
||||||
|
sort_order: 'desc',
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
sources: data,
|
||||||
|
nextOffset: data.length === NOTEBOOK_SOURCES_PAGE_SIZE ? pageParam + data.length : undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialPageParam: 0,
|
||||||
|
getNextPageParam: (lastPage) => lastPage.nextOffset,
|
||||||
|
enabled: !!notebookId,
|
||||||
|
staleTime: 5 * 1000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Flatten all pages into a single array (memoized to prevent infinite re-renders)
|
||||||
|
const sources: SourceListResponse[] = useMemo(
|
||||||
|
() => query.data?.pages.flatMap(page => page.sources) ?? [],
|
||||||
|
[query.data?.pages]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Refetch function that resets to first page
|
||||||
|
const refetch = useCallback(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.sourcesInfinite(notebookId) })
|
||||||
|
}, [queryClient, notebookId])
|
||||||
|
|
||||||
|
return {
|
||||||
|
sources,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
isFetchingNextPage: query.isFetchingNextPage,
|
||||||
|
hasNextPage: query.hasNextPage,
|
||||||
|
fetchNextPage: query.fetchNextPage,
|
||||||
|
refetch,
|
||||||
|
error: query.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useSource(id: string) {
|
export function useSource(id: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: QUERY_KEYS.source(id),
|
queryKey: QUERY_KEYS.source(id),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "open-notebook"
|
name = "open-notebook"
|
||||||
version = "1.2.3"
|
version = "1.2.4"
|
||||||
description = "An open source implementation of a research assistant, inspired by Google Notebook LM"
|
description = "An open source implementation of a research assistant, inspired by Google Notebook LM"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Luis Novo", email = "lfnovo@gmail.com"}
|
{name = "Luis Novo", email = "lfnovo@gmail.com"}
|
||||||
|
|
|
||||||
22
scripts/wait-for-api.sh
Executable file
22
scripts/wait-for-api.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Wait for the API to be healthy before starting the frontend
|
||||||
|
# This prevents the "Unable to Connect to API Server" error during startup
|
||||||
|
|
||||||
|
API_URL="${INTERNAL_API_URL:-http://localhost:5055}"
|
||||||
|
MAX_RETRIES=60 # 60 retries * 5 seconds = 5 minutes max wait
|
||||||
|
RETRY_INTERVAL=5
|
||||||
|
|
||||||
|
echo "Waiting for API to be ready at ${API_URL}/health..."
|
||||||
|
|
||||||
|
for i in $(seq 1 $MAX_RETRIES); do
|
||||||
|
if curl -s -f "${API_URL}/health" > /dev/null 2>&1; then
|
||||||
|
echo "API is ready! Starting frontend..."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Attempt $i/$MAX_RETRIES: API not ready yet, waiting ${RETRY_INTERVAL}s..."
|
||||||
|
sleep $RETRY_INTERVAL
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "ERROR: API did not become ready within $((MAX_RETRIES * RETRY_INTERVAL)) seconds"
|
||||||
|
echo "Starting frontend anyway - users may see connection errors initially"
|
||||||
|
exit 0 # Exit 0 so frontend still starts (better than nothing)
|
||||||
|
|
@ -26,7 +26,7 @@ autostart=true
|
||||||
startsecs=3
|
startsecs=3
|
||||||
|
|
||||||
[program:frontend]
|
[program:frontend]
|
||||||
command=bash -c "sleep 5 && npm run start"
|
command=bash -c "/app/scripts/wait-for-api.sh && npm run start"
|
||||||
directory=/app/frontend
|
directory=/app/frontend
|
||||||
environment=NODE_ENV="production",PORT="8502"
|
environment=NODE_ENV="production",PORT="8502"
|
||||||
passenv=API_URL,NEXT_PUBLIC_API_URL,INTERNAL_API_URL
|
passenv=API_URL,NEXT_PUBLIC_API_URL,INTERNAL_API_URL
|
||||||
|
|
@ -37,4 +37,4 @@ stderr_logfile_maxbytes=0
|
||||||
autorestart=true
|
autorestart=true
|
||||||
priority=30
|
priority=30
|
||||||
autostart=true
|
autostart=true
|
||||||
startsecs=5
|
startsecs=10
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ autostart=true
|
||||||
startsecs=3
|
startsecs=3
|
||||||
|
|
||||||
[program:frontend]
|
[program:frontend]
|
||||||
command=bash -c "sleep 5 && npm run start"
|
command=bash -c "/app/scripts/wait-for-api.sh && npm run start"
|
||||||
directory=/app/frontend
|
directory=/app/frontend
|
||||||
environment=NODE_ENV="production",PORT="8502"
|
environment=NODE_ENV="production",PORT="8502"
|
||||||
passenv=API_URL,NEXT_PUBLIC_API_URL,INTERNAL_API_URL
|
passenv=API_URL,NEXT_PUBLIC_API_URL,INTERNAL_API_URL
|
||||||
|
|
@ -49,4 +49,4 @@ stderr_logfile_maxbytes=0
|
||||||
autorestart=true
|
autorestart=true
|
||||||
priority=30
|
priority=30
|
||||||
autostart=true
|
autostart=true
|
||||||
startsecs=5
|
startsecs=10
|
||||||
Loading…
Reference in a new issue