Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d2540a2
implement native embedder job queue
angelplusultra Mar 10, 2026
e941b17
persist embedding progress across renders
angelplusultra Mar 11, 2026
a0cd78b
add development worker timeouts
angelplusultra Mar 11, 2026
573856a
change to static method
angelplusultra Mar 11, 2026
c80107e
native reranker
angelplusultra Mar 11, 2026
0149020
remove useless return
angelplusultra Mar 11, 2026
acd1e3d
lint
angelplusultra Mar 11, 2026
f4f6b78
simplify
angelplusultra Mar 11, 2026
af3c6e0
make embedding worker timeout value configurable by admin
angelplusultra Mar 11, 2026
592905c
add event emission for missing data
angelplusultra Mar 11, 2026
73ce274
lint
angelplusultra Mar 11, 2026
f0e5117
remove onProgress callback argument
angelplusultra Mar 11, 2026
8c21420
make rerank to rerankDirect
angelplusultra Mar 11, 2026
37ddfaa
persists progress state across app reloads
angelplusultra Mar 11, 2026
0bee3ec
remove chunk level progress reporting
angelplusultra Mar 11, 2026
59d3727
remove unuse dvariable
angelplusultra Mar 11, 2026
6ff2015
make NATIVE_RERANKING_WORKER_TIMEOUT user configurable
angelplusultra Mar 11, 2026
21c5784
remove dead code
angelplusultra Mar 11, 2026
addab25
scope embedding progress per-user and clear stale state on SSE reconnect
angelplusultra Mar 11, 2026
7a80d1e
lint
angelplusultra Mar 11, 2026
c459c45
revert vector databases and embedding engines to call their original …
angelplusultra Mar 11, 2026
c23bb05
simplify rerank
angelplusultra Mar 12, 2026
292b61a
simplify progress fetching by removing updateProgressFromApi
angelplusultra Mar 12, 2026
1718f92
remove duplicate jsdoc
angelplusultra Mar 12, 2026
3fbc217
replace sessionStorage persistence with server-side history replay fo…
angelplusultra Mar 12, 2026
9eff983
fix old comment
angelplusultra Mar 12, 2026
4ae70cb
fix: ignore premature SSE all_complete when embedding hasn't started yet
angelplusultra Mar 12, 2026
3c6d12a
reduce duplication with progress emissions
angelplusultra Mar 12, 2026
2877dc9
remove dead code
angelplusultra Mar 12, 2026
9a15f55
refactor: streamline embedding progress handling
angelplusultra Mar 12, 2026
dce2bf8
fix stale comment
angelplusultra Mar 12, 2026
0722754
remove unused function
angelplusultra Mar 12, 2026
7df5535
fix event emissions for document creation failure
angelplusultra Mar 12, 2026
5195cca
refactor: move Reranking Worker Idle Timeout input to LanceDBOptions …
angelplusultra Mar 12, 2026
c88fac3
lint
angelplusultra Mar 12, 2026
8b99aea
remove unused hadHistory vars
angelplusultra Mar 12, 2026
eafa12c
refactor workspace directory by hoisting component and converting int…
angelplusultra Mar 13, 2026
f335e2b
moved EmbeddingProgressProvider to wrap Document Manager Modal
angelplusultra Mar 13, 2026
2b7bce8
refactor embed progress SSE connection to use fetchEventSource instea…
angelplusultra Mar 13, 2026
4ed2304
refactor message handlng into a function and reduce duplication
angelplusultra Mar 13, 2026
904ed16
refactor: utilize writeResponseChunk for event emissions in document …
angelplusultra Mar 13, 2026
b1d2dd6
refactor: explicit in-proc embedding and rerank methods that are call…
angelplusultra Mar 13, 2026
9a63f98
Abstract EmbeddingProgressBus and Worker Queue into modules
angelplusultra Mar 13, 2026
cf8c681
remove error and toast messages on embed process result
angelplusultra Mar 14, 2026
fd6dc2b
use safeJsonParse
angelplusultra Mar 14, 2026
dd1c0e4
add chunk-level progress events with per-document progress bar in UI
angelplusultra Mar 16, 2026
d27576d
remove unused parameter
angelplusultra Mar 16, 2026
77c85e7
rename all worker timeout references to use ttl | remove ttl updating…
angelplusultra Mar 16, 2026
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
5 changes: 4 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { PWAModeProvider } from "./PWAContext";
import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp";
import { ErrorBoundary } from "react-error-boundary";
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback";
import { EmbeddingProgressProvider } from "./EmbeddingProgressContext";

export default function App() {
const location = useLocation();
Expand All @@ -30,7 +31,9 @@ export default function App() {
<LogoProvider>
<PfpProvider>
<I18nextProvider i18n={i18n}>
<Outlet />
<EmbeddingProgressProvider>
<Outlet />
</EmbeddingProgressProvider>
<ToastContainer />
<KeyboardShortcutsHelp />
</I18nextProvider>
Expand Down
170 changes: 170 additions & 0 deletions frontend/src/EmbeddingProgressContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { API_BASE, AUTH_TOKEN } from "@/utils/constants";

const EmbeddingProgressContext = createContext();

export function useEmbeddingProgress() {
const ctx = useContext(EmbeddingProgressContext);
if (!ctx)
throw new Error(
"useEmbeddingProgress must be used within EmbeddingProgressProvider"
);
return ctx;
}

export function EmbeddingProgressProvider({ children }) {
const [embeddingProgressMap, setEmbeddingProgressMap] = useState({});
const eventSourcesRef = useRef({});
const cleanupTimeoutsRef = useRef({});

// Cleanup all EventSources on unmount
useEffect(() => {
return () => {
for (const slug of Object.keys(eventSourcesRef.current)) {
eventSourcesRef.current[slug]?.close();
}
};
}, []);

/**
* Open (or reconnect) an SSE EventSource for a given workspace slug.
* Updates embeddingProgressMap in real time as events arrive.
*/
const connectSSE = useCallback((slug) => {
// Don't double-connect
if (eventSourcesRef.current[slug]) return;

try {
const token = window.localStorage.getItem(AUTH_TOKEN);
const progressUrl = new URL(
`${API_BASE}/workspace/${slug}/embed-progress`
);
if (token) progressUrl.searchParams.set("token", token);

const eventSource = new EventSource(progressUrl.toString());
eventSourcesRef.current[slug] = eventSource;

eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);

switch (data.type) {
case "batch_starting": {
const initial = {};
for (const name of data.filenames || []) {
initial[name] = { status: "pending" };
}
setEmbeddingProgressMap((prev) => ({
...prev,
[slug]: { ...initial, ...prev[slug] },
}));
break;
}

case "doc_starting":
setEmbeddingProgressMap((prev) => ({
...prev,
[slug]: {
...prev[slug],
[data.filename]: { status: "embedding" },
},
}));
break;

case "doc_complete":
setEmbeddingProgressMap((prev) => ({
...prev,
[slug]: {
...prev[slug],
[data.filename]: { status: "complete" },
},
}));
break;

case "doc_failed":
setEmbeddingProgressMap((prev) => ({
...prev,
[slug]: {
...prev[slug],
[data.filename]: {
status: "failed",
error: data.error || "Embedding failed",
},
},
}));
break;

case "all_complete":
// A real embedding job just finished β€” close and schedule cleanup.
eventSource.close();
delete eventSourcesRef.current[slug];
cleanupTimeoutsRef.current[slug] = setTimeout(() => {
setEmbeddingProgressMap((prev) => {
const next = { ...prev };
delete next[slug];
return next;
});
delete cleanupTimeoutsRef.current[slug];
}, 5000);
break;
}
} catch {
// ignore parse errors
}
};

eventSource.onerror = () => {
eventSource.close();
delete eventSourcesRef.current[slug];
};
} catch {
// SSE is optional β€” embedding still works without it
}
}, []);

const startEmbedding = useCallback(
(slug, filenames) => {
// Close any existing EventSource for this slug
if (eventSourcesRef.current[slug]) {
eventSourcesRef.current[slug].close();
delete eventSourcesRef.current[slug];
}
if (cleanupTimeoutsRef.current[slug]) {
clearTimeout(cleanupTimeoutsRef.current[slug]);
delete cleanupTimeoutsRef.current[slug];
}

// Set all filenames to pending
const initialProgress = {};
for (const name of filenames) {
initialProgress[name] = { status: "pending" };
}
setEmbeddingProgressMap((prev) => ({
...prev,
[slug]: { ...initialProgress },
}));

connectSSE(slug);
},
[connectSSE]
);

return (
<EmbeddingProgressContext.Provider
value={{
embeddingProgressMap,
startEmbedding,
connectSSE,
}}
>
{children}
</EmbeddingProgressContext.Provider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,30 @@ export default function NativeEmbeddingOptions({ settings }) {
</div>
)}
</div>
<div className="w-full flex flex-col mt-1.5">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-1">
Worker Idle Timeout
</label>
<p className="text-theme-text-secondary text-xs font-normal block mb-3">
How long the embedding worker process stays alive after finishing
work. Set to 0 to shut down immediately.
</p>
<input
type="number"
name="NativeEmbeddingWorkerTimeout"
min={0}
max={3600}
step={1}
defaultValue={settings?.NativeEmbeddingWorkerTimeout ?? ""}
className="border-none bg-theme-settings-input-bg border-gray-500 text-theme-text-primary text-sm rounded-lg block w-full p-2.5"
placeholder="300"
/>
<p className="text-theme-text-secondary text-xs font-normal mt-1">
Value in seconds. Leave empty to use the default (300s).
</p>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@ import { dollarFormat } from "@/utils/numbers";
import WorkspaceFileRow from "./WorkspaceFileRow";
import { memo, useEffect, useState } from "react";
import ModalWrapper from "@/components/ModalWrapper";
import { Eye, PushPin } from "@phosphor-icons/react";
import {
Eye,
PushPin,
CheckCircle,
XCircle,
CircleNotch,
Clock,
} from "@phosphor-icons/react";
import { SEEN_DOC_PIN_ALERT, SEEN_WATCH_ALERT } from "@/utils/constants";
import paths from "@/utils/paths";
import { Link } from "react-router-dom";
import Workspace from "@/models/workspace";
import { Tooltip } from "react-tooltip";
import { safeJsonParse } from "@/utils/request";
import { useTranslation } from "react-i18next";
import { middleTruncate } from "@/utils/directories";

function WorkspaceDirectory({
workspace,
Expand All @@ -25,6 +33,7 @@ function WorkspaceDirectory({
saveChanges,
embeddingCosts,
movedItems,
embeddingProgress = null,
}) {
const { t } = useTranslation();
const [selectedItems, setSelectedItems] = useState({});
Expand Down Expand Up @@ -87,7 +96,7 @@ function WorkspaceDirectory({
saveChanges(e);
};

if (loading) {
if (loading || embeddingProgress) {
return (
<div className="px-8">
<div className="flex items-center justify-start w-[560px]">
Expand All @@ -101,14 +110,31 @@ function WorkspaceDirectory({
<div className="shrink-0 w-3 h-3" />
<p className="ml-[7px] text-theme-text-primary">Name</p>
</div>
<p className="col-span-2" />
</div>
<div className="w-full h-[calc(100%-40px)] flex items-center justify-center flex-col gap-y-5">
<PreLoader />
<p className="text-theme-text-primary text-sm font-semibold animate-pulse text-center w-1/3">
{loadingMessage}
<p className="col-span-2 text-right text-theme-text-primary">
Status
</p>
</div>

{embeddingProgress ? (
<div className="overflow-y-auto h-[calc(100%-40px)]">
{Object.entries(embeddingProgress).map(
([filename, fileStatus]) => (
<EmbeddingFileRow
key={filename}
filename={filename}
status={fileStatus}
/>
)
)}
</div>
) : (
<div className="w-full h-[calc(100%-40px)] flex items-center justify-center flex-col gap-y-5">
<PreLoader />
<p className="text-theme-text-primary text-sm font-semibold animate-pulse text-center w-1/3">
{loadingMessage}
</p>
</div>
)}
</div>
</div>
);
Expand Down Expand Up @@ -467,4 +493,75 @@ function WorkspaceDocumentTooltips() {
);
}

function EmbeddingFileRow({ filename, status }) {
// Extract a readable display name from the filename path
// e.g. "custom-documents/my-doc.json" -> "my-doc"
const displayName =
filename
.split("/")
.pop()
?.replace(/\.json$/, "") || filename;

const statusIcon = {
pending: (
<Clock size={16} className="text-white/40 shrink-0" weight="regular" />
),
embedding: (
<CircleNotch
size={16}
className="text-sky-400 animate-spin shrink-0"
weight="bold"
/>
),
complete: (
<CheckCircle
size={16}
className="text-green-400 shrink-0"
weight="fill"
/>
),
failed: (
<XCircle size={16} className="text-red-400 shrink-0" weight="fill" />
),
};

const statusLabel = {
pending: "Queued",
embedding: "Embedding...",
complete: "Complete",
failed: "Failed",
};

return (
<div className="text-theme-text-primary text-xs grid grid-cols-12 py-2 pl-3.5 pr-3.5 h-[34px] items-center border-b border-white/5">
<div className="col-span-8 flex items-center gap-x-2 overflow-hidden">
{statusIcon[status.status] || statusIcon.pending}
<p
className={`whitespace-nowrap overflow-hidden text-ellipsis ${
status.status === "failed" ? "text-red-400" : ""
}`}
title={displayName}
>
{middleTruncate(displayName, 45)}
</p>
</div>
<div className="col-span-4 flex justify-end items-center gap-x-2">
<p
className={`text-[10px] whitespace-nowrap ${
status.status === "failed"
? "text-red-400"
: status.status === "complete"
? "text-green-400"
: status.status === "embedding"
? "text-sky-400"
: "text-white/40"
}`}
>
{statusLabel[status.status] || "Queued"}
</p>
</div>
</div>
);
}

export default memo(WorkspaceDirectory);
Loading
Loading