feat: make message items in search open the message

This commit is contained in:
SethBurkart123
2025-05-22 14:49:13 +10:00
parent 6846d945f2
commit efdd03ce8e
2 changed files with 91 additions and 80 deletions
@@ -1,4 +1,7 @@
import { waitForElm } from "@/seqta/utils/waitForElm";
import type { IndexItem } from "./types"; import type { IndexItem } from "./types";
import ReactFiber from "@/seqta/utils/ReactFiber";
import { delay } from "@/seqta/utils/delay";
interface MessageMetadata { interface MessageMetadata {
messageId: number; messageId: number;
@@ -26,13 +29,45 @@ interface AssessmentMetadata {
type ActionHandler<T = any> = (item: IndexItem & { metadata: T }) => void; type ActionHandler<T = any> = (item: IndexItem & { metadata: T }) => void;
export const actionMap: Record<string, ActionHandler<any>> = { export const actionMap: Record<string, ActionHandler<any>> = {
message: ((item: IndexItem & { metadata: MessageMetadata }) => { message: (async (item: IndexItem & { metadata: MessageMetadata }) => {
window.location.hash = `#?page=/messages&id=${item.metadata.messageId}`; window.location.hash = `#?page=/messages`;
await waitForElm('[class*="Viewer__Viewer___"] > div', true, 20);
// Select the specific direct message
ReactFiber.find('[class*="Viewer__Viewer___"] > div').setState({
selected: new Set([item.metadata.messageId]),
});
// send a network request to mark as read
fetch('/seqta/student/save/message', {
method: "POST",
credentials: "include",
body: JSON.stringify({
items: [item.metadata.messageId],
mode: 'x-read',
read: true,
}),
});
await delay(10);
const button = document.querySelector('[class*="MessageList__selected___"]');
if (button) {
(button as HTMLElement).click();
}
}) as ActionHandler<any>, }) as ActionHandler<any>,
assessment: ((item: IndexItem & { metadata: AssessmentMetadata }) => { assessment: (async (item: IndexItem & { metadata: AssessmentMetadata }) => {
if (item.metadata.isMessageBased) { if (item.metadata.isMessageBased) {
window.location.hash = `#?page=/messages&id=${item.metadata.messageId}`; window.location.hash = `#?page=/messages`;
await waitForElm('[class*="Viewer__Viewer___"] > div', true, 20);
// Select the specific direct message
ReactFiber.find('[class*="Viewer__Viewer___"] > div').setState({
selected: new Set([item.metadata.messageId]),
});
} else { } else {
window.location.hash = `#?page=/assessments&id=${item.metadata.assessmentId}`; window.location.hash = `#?page=/assessments&id=${item.metadata.assessmentId}`;
} }
@@ -100,19 +100,19 @@ export async function loadAllStoredItems(): Promise<IndexItem[]> {
item.text && item.text &&
item.category && item.category &&
item.actionId && item.actionId &&
job.renderComponentId job.renderComponentId // job might not be defined if store exists but job was removed
) { ) {
all.push(item); all.push(item);
} else { } else {
console.warn(`Skipping invalid item from job ${jobId}:`, item); console.warn(`Skipping invalid item from job store ${jobId}:`, item);
} }
} }
} catch (error) { } catch (error) {
console.error(`Error loading items for job ${jobId}:`, error); console.error(`Error loading items for job store ${jobId}:`, error);
} }
} }
console.debug( console.debug(
`[Indexer] Loaded ${all.length} items from non-vector storage.`, `[Indexer] Loaded ${all.length} items from all primary stores.`,
); );
return all; return all;
} }
@@ -135,8 +135,6 @@ export async function runIndexing(): Promise<void> {
const totalSteps = jobIds.length + 1; const totalSteps = jobIds.length + 1;
dispatchProgress(completedJobs, totalSteps, true, "Starting jobs"); dispatchProgress(completedJobs, totalSteps, true, "Starting jobs");
const allItemsFromJobs: IndexItem[] = [];
// --- Step 1: Run Fetching/Storing Jobs (Main Thread) --- // --- Step 1: Run Fetching/Storing Jobs (Main Thread) ---
for (const jobId of jobIds) { for (const jobId of jobIds) {
dispatchProgress( dispatchProgress(
@@ -204,21 +202,18 @@ export async function runIndexing(): Promise<void> {
console.debug(`%c[Indexer] Running job "${jobId}"...`, "color: #4ea1ff"); console.debug(`%c[Indexer] Running job "${jobId}"...`, "color: #4ea1ff");
try { try {
const newItemsRaw = await job.run(ctx); const newItemsRaw = await job.run(ctx); // newItemsRaw are items *returned* by the job.
// Some jobs (like messages) might add via ctx.addItem and return [].
const stored = await getStoredItems(); const stored = await getStoredItems();
let merged = mergeItems(stored, newItemsRaw); let merged = mergeItems(stored, newItemsRaw);
if (job.purge) merged = job.purge(merged); if (job.purge) merged = job.purge(merged);
console.log(`[Indexer] ${job.label}: ${merged.length} items stored in '${jobId}' store (non-vector).`);
await setStoredItems(merged); await setStoredItems(merged);
await updateLastRunMeta(jobId); await updateLastRunMeta(jobId);
allItemsFromJobs.push(...newItemsRaw);
console.debug( console.debug(
`%c[Indexer] ${job.label}: ${newItemsRaw.length} new items from run, ${merged.length} total stored in '${jobId}' store (non-vector).`, `%c[Indexer] ${job.label}: ${newItemsRaw.length} new items reported by run, ${merged.length} total items now in '${jobId}' store.`,
"color: #00c46f", "color: #00c46f",
); );
} catch (err) { } catch (err) {
@@ -236,18 +231,19 @@ export async function runIndexing(): Promise<void> {
} }
// --- Step 2: Delegate Vectorization to Worker (Off Main Thread) --- // --- Step 2: Delegate Vectorization to Worker (Off Main Thread) ---
if (allItemsFromJobs.length > 0) { // Load ALL items from the primary stores. The worker will handle deduplication against its own vector store.
const allItemsInPrimaryStores = await loadAllStoredItems();
if (allItemsInPrimaryStores.length > 0) {
console.debug( console.debug(
`%c[Indexer] Sending ${allItemsFromJobs.length} items to worker for vectorization...`, `%c[Indexer] Sending ${allItemsInPrimaryStores.length} items from primary stores to worker for vectorization check...`,
"color: #4ea1ff", "color: #4ea1ff",
); );
dispatchProgress(completedJobs, totalSteps, true, "Starting vectorization"); dispatchProgress(completedJobs, totalSteps, true, "Starting vectorization of stored items");
try { try {
const workerManager = VectorWorkerManager.getInstance(); const workerManager = VectorWorkerManager.getInstance();
// Pass a progress callback to the worker manager await workerManager.processItems(allItemsInPrimaryStores, (progress) => {
await workerManager.processItems(allItemsFromJobs, (progress) => {
// Update overall progress based on worker feedback
let detailMessage = progress.message || ""; let detailMessage = progress.message || "";
if ( if (
progress.status === "processing" && progress.status === "processing" &&
@@ -255,26 +251,26 @@ export async function runIndexing(): Promise<void> {
progress.processed !== undefined progress.processed !== undefined
) { ) {
detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`; detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`;
// You could potentially update the 'completed' count more granularly here
// For simplicity, we'll just update the detail message
} else if (progress.status === "complete") { } else if (progress.status === "complete") {
detailMessage = "Vectorization complete"; detailMessage = "Vectorization complete";
// Mark the vectorization step as complete // Mark the vectorization step as complete
completedJobs++; // Increment completion count *after* vectorization finishes
dispatchProgress( dispatchProgress(
completedJobs,
totalSteps, totalSteps,
totalSteps, false, // Indexing finished
true, "Indexing finished",
"Vectorization finished", detailMessage
); );
} else if (progress.status === "error") { } else if (progress.status === "error") {
detailMessage = `Vectorization error: ${progress.message}`; detailMessage = `Vectorization error: ${progress.message}`;
dispatchProgress( dispatchProgress(
completedJobs, completedJobs,
totalSteps, totalSteps,
true, false, // Indexing stopped
"Vectorization failed", "Vectorization failed",
detailMessage, detailMessage,
); // Show error );
} else if (progress.status === "started") { } else if (progress.status === "started") {
detailMessage = `Vectorization started for ${progress.total} items`; detailMessage = `Vectorization started for ${progress.total} items`;
} else if (progress.status === "cancelled") { } else if (progress.status === "cancelled") {
@@ -282,49 +278,27 @@ export async function runIndexing(): Promise<void> {
dispatchProgress( dispatchProgress(
completedJobs, completedJobs,
totalSteps, totalSteps,
true, false, // Indexing stopped
"Vectorization cancelled", "Vectorization cancelled",
detailMessage, detailMessage,
); );
} }
// Update the status detail // Update the status detail for ongoing vectorization
dispatchProgress( if (progress.status !== "complete" && progress.status !== "error" && progress.status !== "cancelled") {
completedJobs, dispatchProgress(
totalSteps, completedJobs, // Still on job completion count
true, totalSteps,
"Vectorization in progress", true, // Indexing still active
detailMessage, "Vectorization in progress",
); detailMessage,
);
// When worker signals completion of *its* task, mark the final step complete
if (progress.status === "complete") {
completedJobs++; // Increment completion count *after* vectorization finishes
dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing finished",
); // Set indexing to false
} else if (
progress.status === "error" ||
progress.status === "cancelled"
) {
// Don't increment completed count on failure/cancel, just stop indexing indicator
dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing stopped due to error/cancel",
);
} }
}); });
console.debug( console.debug(
"%c[Indexer] Vectorization task sent to worker.", "%c[Indexer] Vectorization task for stored items sent to worker.",
"color: green", "color: green",
); );
// Note: runIndexing might return *before* vectorization is complete now.
// The progress updates will signal the true end state.
} catch (error) { } catch (error) {
console.error( console.error(
`%c[Indexer] ❌ Failed to send items to vector worker:`, `%c[Indexer] ❌ Failed to send items to vector worker:`,
@@ -334,41 +308,43 @@ export async function runIndexing(): Promise<void> {
dispatchProgress( dispatchProgress(
completedJobs, completedJobs,
totalSteps, totalSteps,
false, false, // Indexing stopped
"Vectorization failed", "Vectorization failed",
String(error), String(error),
); // Stop indexing indicator );
} }
} else { } else {
console.debug( console.debug(
"%c[Indexer] No items to send for vectorization.", "%c[Indexer] No items found in primary stores to send for vectorization.",
"color: gray", "color: gray",
); );
// If no vectorization needed, indexing is done here.
completedJobs++; // Count the "skipped" vectorization step completedJobs++; // Count the "skipped" vectorization step
dispatchProgress( dispatchProgress(
completedJobs, completedJobs,
totalSteps, totalSteps,
false, false, // Indexing finished
"Indexing finished (no vectorization needed)", "Indexing finished (no items for vectorization)",
); );
} }
// Stop heartbeat ONLY when all jobs *and* the vectorization dispatch are done.
// The actual *completion* of vectorization is now asynchronous.
stopHeartbeat(); stopHeartbeat();
// Before loading dynamic items, attach renderComponent to each item if available // Update dynamic items with everything that's now in the primary stores
allItemsFromJobs.forEach(item => { // These items are either already vectorized or will be by the worker.
const renderComponent = renderComponentMap[item.renderComponentId]; allItemsInPrimaryStores.forEach(item => {
if (renderComponent) { // Ensure job still exists for renderComponentId mapping
item.renderComponent = renderComponent; const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
if (jobDef) {
const renderComponent = renderComponentMap[jobDef.renderComponentId];
if (renderComponent) {
item.renderComponent = renderComponent;
}
} else if (renderComponentMap[item.renderComponentId]) { // Fallback if category doesn't match a job id directly
item.renderComponent = renderComponentMap[item.renderComponentId];
} }
}); });
loadDynamicItems(allItemsFromJobs); loadDynamicItems(allItemsInPrimaryStores);
window.dispatchEvent(new Event("dynamic-items-updated")); window.dispatchEvent(new Event("dynamic-items-updated"));
// Final progress update might be handled by the worker callback now.
// dispatchProgress(completedJobs, totalSteps, false); // This might be premature
} }
function mergeItems(existing: IndexItem[], incoming: IndexItem[]): IndexItem[] { function mergeItems(existing: IndexItem[], incoming: IndexItem[]): IndexItem[] {