mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-02 19:55:18 +02:00
Merge remote-tracking branch 'upstream/dev' into feat/whatsapp-gateway-integration
This commit is contained in:
commit
2e64d5d3de
15 changed files with 472 additions and 136 deletions
|
|
@ -15,7 +15,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component
|
|||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { RunStepResult } from "@/contracts/types/automation.types";
|
||||
import type { RunStatus, RunStepResult } from "@/contracts/types/automation.types";
|
||||
import { useAutomationRun } from "@/hooks/use-automation-runs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RunStepResultCard } from "./run-step-result-card";
|
||||
|
|
@ -23,44 +23,46 @@ import { RunStepResultCard } from "./run-step-result-card";
|
|||
interface RunDetailsPanelProps {
|
||||
automationId: number;
|
||||
runId: number;
|
||||
/** Live step entries from Zero; rendered while the run is in-flight and
|
||||
* also kept as the authoritative source once it finishes. */
|
||||
liveSteps: RunStepResult[];
|
||||
/** Live run status from Zero. Used to hide diagnostic sections that
|
||||
* only make sense after the run reaches a terminal state. */
|
||||
liveStatus: RunStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expanded view of a single run. Fetches lazily — the parent only renders
|
||||
* this once the row is opened, so the list view stays cheap.
|
||||
* Expanded view of a single run. Steps render immediately from the live
|
||||
* Zero row so the panel updates as the run progresses; the heavy REST
|
||||
* payload (output, artifacts, resolved inputs, run-level error) is
|
||||
* fetched lazily and merged in when it arrives.
|
||||
*
|
||||
* We surface the run outcome readably: a run-level error first (when
|
||||
* present), then per-step cards that render the agent's markdown
|
||||
* ``final_message`` directly, and finally the structural artifacts/inputs.
|
||||
* The full ``definition_snapshot`` is omitted because it usually mirrors the
|
||||
* live definition — surfacing it would dominate the panel without informing
|
||||
* Surfacing order is outcome-first: a run-level error (when present),
|
||||
* then per-step cards that render the agent's markdown ``final_message``
|
||||
* directly, and finally the structural artifacts/inputs. The full
|
||||
* ``definition_snapshot`` is omitted because it usually mirrors the live
|
||||
* definition — surfacing it would dominate the panel without informing
|
||||
* what the user is trying to learn ("did this work? what did it do?").
|
||||
*/
|
||||
export function RunDetailsPanel({ automationId, runId }: RunDetailsPanelProps) {
|
||||
const { data: run, isLoading, error } = useAutomationRun(automationId, runId);
|
||||
export function RunDetailsPanel({
|
||||
automationId,
|
||||
runId,
|
||||
liveSteps,
|
||||
liveStatus,
|
||||
}: RunDetailsPanelProps) {
|
||||
const isTerminal = liveStatus !== "pending" && liveStatus !== "running";
|
||||
// Defer the REST round-trip until the run can actually carry heavy
|
||||
// fields — output/artifacts/error are only written at terminal mark.
|
||||
const { data: run, isLoading, error } = useAutomationRun(automationId, runId, {
|
||||
enabled: isTerminal,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 border-t border-border/60 bg-muted/20 p-4">
|
||||
<Skeleton className="h-3 w-32" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !run) {
|
||||
return (
|
||||
<div className="border-t border-border/60 bg-muted/20 p-4 text-xs text-muted-foreground">
|
||||
Couldn't load run details{error?.message ? `: ${error.message}` : "."}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const runError = run.error && Object.keys(run.error).length > 0 ? run.error : null;
|
||||
const hasOutput = run.output && Object.keys(run.output).length > 0;
|
||||
const hasInputs = Object.keys(run.inputs ?? {}).length > 0;
|
||||
const steps = run.step_results as RunStepResult[];
|
||||
const hasDiagnostics = run.artifacts.length > 0 || hasInputs;
|
||||
const runError = run?.error && Object.keys(run.error).length > 0 ? run.error : null;
|
||||
const hasOutput = !!run?.output && Object.keys(run.output).length > 0;
|
||||
const hasInputs = !!run && Object.keys(run.inputs ?? {}).length > 0;
|
||||
const hasDiagnostics = !!run && (run.artifacts.length > 0 || hasInputs);
|
||||
const heavyLoading = isTerminal && isLoading && !run;
|
||||
const heavyError = isTerminal && !!error;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 border-t border-border/60 bg-muted/20 p-4">
|
||||
|
|
@ -72,30 +74,40 @@ export function RunDetailsPanel({ automationId, runId }: RunDetailsPanelProps) {
|
|||
</Section>
|
||||
) : null}
|
||||
|
||||
<Section icon={GitCommitHorizontal} label={`Step results · ${steps.length}`}>
|
||||
{steps.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No steps recorded.</p>
|
||||
<Section icon={GitCommitHorizontal} label={`Step results · ${liveSteps.length}`}>
|
||||
{liveSteps.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isTerminal ? "No steps recorded." : "Waiting for first step…"}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{steps.map((step, index) => (
|
||||
{liveSteps.map((step, index) => (
|
||||
<RunStepResultCard key={step.step_id ?? index} step={step} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{hasDiagnostics ? <Separator className="bg-border/60" /> : null}
|
||||
|
||||
{run.artifacts.length > 0 ? (
|
||||
<Section icon={Package} label={`Artifacts · ${run.artifacts.length}`}>
|
||||
<JsonBlock value={run.artifacts} />
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
{hasInputs ? (
|
||||
<Section icon={Settings2} label="Resolved inputs">
|
||||
<JsonBlock value={run.inputs} />
|
||||
</Section>
|
||||
{heavyLoading ? (
|
||||
<Skeleton className="h-16 w-full" />
|
||||
) : heavyError ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Couldn't load run details{error?.message ? `: ${error.message}` : "."}
|
||||
</p>
|
||||
) : hasDiagnostics ? (
|
||||
<>
|
||||
<Separator className="bg-border/60" />
|
||||
{run && run.artifacts.length > 0 ? (
|
||||
<Section icon={Package} label={`Artifacts · ${run.artifacts.length}`}>
|
||||
<JsonBlock value={run.artifacts} />
|
||||
</Section>
|
||||
) : null}
|
||||
{hasInputs ? (
|
||||
<Section icon={Settings2} label="Resolved inputs">
|
||||
<JsonBlock value={run?.inputs} />
|
||||
</Section>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
"use client";
|
||||
import { ChevronDown, ChevronRight, Hand } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { RunSummary } from "@/contracts/types/automation.types";
|
||||
import type { LiveRunSummary } from "@/hooks/use-automation-runs";
|
||||
import { formatDuration } from "@/lib/automations/run-duration";
|
||||
import { formatRelativeDate } from "@/lib/format-date";
|
||||
import { RunDetailsPanel } from "./run-details-panel";
|
||||
import { RunStatusBadge } from "./run-status-badge";
|
||||
|
||||
interface RunRowProps {
|
||||
run: RunSummary;
|
||||
run: LiveRunSummary;
|
||||
automationId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* One run row. Click to expand → fetches the full run and shows the
|
||||
* details panel inline. State is local to each row so multiple panels
|
||||
* can be open at once (or none).
|
||||
* One run row. Click to expand → renders the details panel inline.
|
||||
* Status and step_results come live from the parent's Zero query; the
|
||||
* panel itself only fetches the heavy REST fields on first expand.
|
||||
*/
|
||||
export function RunRow({ run, automationId }: RunRowProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
|
@ -47,7 +47,14 @@ export function RunRow({ run, automationId }: RunRowProps) {
|
|||
</div>
|
||||
</button>
|
||||
|
||||
{open && <RunDetailsPanel automationId={automationId} runId={run.id} />}
|
||||
{open && (
|
||||
<RunDetailsPanel
|
||||
automationId={automationId}
|
||||
runId={run.id}
|
||||
liveSteps={run.step_results}
|
||||
liveStatus={run.status}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ Zero syncs the following tables for real-time features:
|
|||
| `new_chat_messages` | Live chat message sync for shared chats |
|
||||
| `chat_comments` | Real-time comment threads on AI responses |
|
||||
| `chat_session_state` | Collaboration indicators (who is typing) |
|
||||
| `automation_runs` | Live run status and per-step progress (thin column set; heavy fields stay on REST) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -1528,14 +1528,20 @@ export const LLM_MODELS: LLMModel[] = [
|
|||
|
||||
// MiniMax
|
||||
{
|
||||
value: "MiniMax-M2.5",
|
||||
label: "MiniMax M2.5",
|
||||
value: "MiniMax-M3",
|
||||
label: "MiniMax M3",
|
||||
provider: "MINIMAX",
|
||||
contextWindow: "512K",
|
||||
},
|
||||
{
|
||||
value: "MiniMax-M2.7",
|
||||
label: "MiniMax M2.7",
|
||||
provider: "MINIMAX",
|
||||
contextWindow: "204K",
|
||||
},
|
||||
{
|
||||
value: "MiniMax-M2.5-highspeed",
|
||||
label: "MiniMax M2.5 Highspeed",
|
||||
value: "MiniMax-M2.7-highspeed",
|
||||
label: "MiniMax M2.7 Highspeed",
|
||||
provider: "MINIMAX",
|
||||
contextWindow: "204K",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -184,8 +184,8 @@ export const LLM_PROVIDERS: LLMProvider[] = [
|
|||
{
|
||||
value: "MINIMAX",
|
||||
label: "MiniMax",
|
||||
example: "MiniMax-M2.5, MiniMax-M2.5-highspeed",
|
||||
description: "High-performance models with 204K context",
|
||||
example: "MiniMax-M3, MiniMax-M2.7",
|
||||
description: "High-performance models with up to 512K context",
|
||||
apiBase: "https://api.minimax.io/v1",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,42 +1,109 @@
|
|||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Run, RunListResponse } from "@/contracts/types/automation.types";
|
||||
import { useQuery as useZeroQuery } from "@rocicorp/zero/react";
|
||||
import { useQuery as useReactQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import type { Run, RunStepResult, RunSummary } from "@/contracts/types/automation.types";
|
||||
import { automationsApiService } from "@/lib/apis/automations-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { queries } from "@/zero/queries";
|
||||
|
||||
const DEFAULT_LIMIT = 50;
|
||||
const DEFAULT_OFFSET = 0;
|
||||
|
||||
/**
|
||||
* Thin live row sourced from Zero. Strict superset of {@link RunSummary} —
|
||||
* existing consumers that only look at the summary fields keep working,
|
||||
* while the run detail panel can read ``step_results`` directly for the
|
||||
* live step ticker without a second REST round-trip.
|
||||
*/
|
||||
export interface LiveRunSummary extends RunSummary {
|
||||
step_results: RunStepResult[];
|
||||
}
|
||||
|
||||
export interface UseAutomationRunsOptions {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/** Paginated run history for one automation. Newest-first per backend. */
|
||||
interface UseAutomationRunsResult {
|
||||
data: { items: LiveRunSummary[]; total: number } | undefined;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Live run history for one automation, newest-first. Sourced from Zero's
|
||||
* thin ``automation_runs`` publication so status and per-step progress
|
||||
* tick in real time without polling. Heavy fields (output, artifacts,
|
||||
* inputs, error, definition_snapshot) are still fetched lazily via
|
||||
* {@link useAutomationRun}.
|
||||
*/
|
||||
export function useAutomationRuns(
|
||||
automationId: number | undefined,
|
||||
{ limit = DEFAULT_LIMIT, offset = DEFAULT_OFFSET, enabled = true }: UseAutomationRunsOptions = {}
|
||||
) {
|
||||
return useQuery<RunListResponse, Error>({
|
||||
queryKey: cacheKeys.automations.runs(automationId ?? 0, limit, offset),
|
||||
queryFn: () => automationsApiService.listRuns(automationId as number, { limit, offset }),
|
||||
enabled: enabled && !!automationId,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
{ limit = DEFAULT_LIMIT }: UseAutomationRunsOptions = {}
|
||||
): UseAutomationRunsResult {
|
||||
const [rows, result] = useZeroQuery(
|
||||
queries.automationRuns.byAutomation({ automationId: automationId ?? -1 })
|
||||
);
|
||||
|
||||
const items = useMemo<LiveRunSummary[]>(() => {
|
||||
if (!automationId) return [];
|
||||
return rows.slice(0, limit).map(toLiveRunSummary);
|
||||
}, [automationId, rows, limit]);
|
||||
|
||||
const total = automationId ? rows.length : 0;
|
||||
|
||||
// Pre-hydration window: nothing visible AND Zero hasn't confirmed
|
||||
// completeness yet. After the first sync (even an empty set) we stop
|
||||
// showing the skeleton so the empty-state copy can take over.
|
||||
const isLoading = !!automationId && result.type !== "complete" && rows.length === 0;
|
||||
|
||||
return {
|
||||
data: automationId ? { items, total } : undefined,
|
||||
isLoading,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Single run with the full snapshot, step results, output and artifacts. */
|
||||
/**
|
||||
* Full run record (definition snapshot, inputs, output, artifacts, error).
|
||||
* Stays on REST: these fields are large and largely static after the run
|
||||
* finishes, so they're not worth replicating to every connected client.
|
||||
*/
|
||||
export function useAutomationRun(
|
||||
automationId: number | undefined,
|
||||
runId: number | undefined,
|
||||
options: { enabled?: boolean } = {}
|
||||
) {
|
||||
const { enabled = true } = options;
|
||||
return useQuery<Run, Error>({
|
||||
return useReactQuery<Run, Error>({
|
||||
queryKey: cacheKeys.automations.run(automationId ?? 0, runId ?? 0),
|
||||
queryFn: () => automationsApiService.getRun(automationId as number, runId as number),
|
||||
enabled: enabled && !!automationId && !!runId,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
interface ZeroAutomationRunRow {
|
||||
id: number;
|
||||
automationId: number;
|
||||
triggerId?: number | null;
|
||||
status: string;
|
||||
stepResults: unknown;
|
||||
startedAt?: number | null;
|
||||
finishedAt?: number | null;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/** Adapt a Zero camelCase row (epoch ms timestamps) to the snake_case
|
||||
* ISO-string ``RunSummary`` shape the existing UI already consumes. */
|
||||
function toLiveRunSummary(row: ZeroAutomationRunRow): LiveRunSummary {
|
||||
return {
|
||||
id: row.id,
|
||||
automation_id: row.automationId,
|
||||
trigger_id: row.triggerId ?? null,
|
||||
status: row.status as RunSummary["status"],
|
||||
started_at: row.startedAt ? new Date(row.startedAt).toISOString() : null,
|
||||
finished_at: row.finishedAt ? new Date(row.finishedAt).toISOString() : null,
|
||||
created_at: new Date(row.createdAt).toISOString(),
|
||||
step_results: Array.isArray(row.stepResults) ? (row.stepResults as RunStepResult[]) : [],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
14
surfsense_web/zero/queries/automations.ts
Normal file
14
surfsense_web/zero/queries/automations.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { defineQuery } from "@rocicorp/zero";
|
||||
import { z } from "zod";
|
||||
import { zql } from "../schema/index";
|
||||
|
||||
// Mirrors chat byThread: client passes the parent id, the REST route still
|
||||
// authorizes via `automation_id -> search_space`. No search_space_id on the
|
||||
// table by design.
|
||||
export const automationRunQueries = {
|
||||
byAutomation: defineQuery(
|
||||
z.object({ automationId: z.number() }),
|
||||
({ args: { automationId } }) =>
|
||||
zql.automation_runs.where("automationId", automationId).orderBy("createdAt", "desc")
|
||||
),
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { defineQueries } from "@rocicorp/zero";
|
||||
import { automationRunQueries } from "./automations";
|
||||
import { chatSessionQueries, commentQueries, messageQueries } from "./chat";
|
||||
import { connectorQueries, documentQueries } from "./documents";
|
||||
import { folderQueries } from "./folders";
|
||||
|
|
@ -14,4 +15,5 @@ export const queries = defineQueries({
|
|||
comments: commentQueries,
|
||||
chatSession: chatSessionQueries,
|
||||
user: userQueries,
|
||||
automationRuns: automationRunQueries,
|
||||
});
|
||||
|
|
|
|||
18
surfsense_web/zero/schema/automations.ts
Normal file
18
surfsense_web/zero/schema/automations.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { json, number, string, table } from "@rocicorp/zero";
|
||||
|
||||
// Thin live row: status + per-step progress only. Heavy fields
|
||||
// (definition_snapshot, inputs, output, artifacts, error) stay on REST
|
||||
// (`GET /automations/{id}/runs/{run_id}`) and load on detail expand.
|
||||
// Mirrors the publication shape in migration 148.
|
||||
export const automationRunTable = table("automation_runs")
|
||||
.columns({
|
||||
id: number(),
|
||||
automationId: number().from("automation_id"),
|
||||
triggerId: number().optional().from("trigger_id"),
|
||||
status: string(),
|
||||
stepResults: json().from("step_results"),
|
||||
startedAt: number().optional().from("started_at"),
|
||||
finishedAt: number().optional().from("finished_at"),
|
||||
createdAt: number().from("created_at"),
|
||||
})
|
||||
.primaryKey("id");
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { createBuilder, createSchema, relationships } from "@rocicorp/zero";
|
||||
import { automationRunTable } from "./automations";
|
||||
import { chatCommentTable, chatSessionStateTable, newChatMessageTable } from "./chat";
|
||||
import { documentTable, searchSourceConnectorTable } from "./documents";
|
||||
import { folderTable } from "./folders";
|
||||
|
|
@ -36,6 +37,7 @@ export const schema = createSchema({
|
|||
chatCommentTable,
|
||||
chatSessionStateTable,
|
||||
userTable,
|
||||
automationRunTable,
|
||||
],
|
||||
relationships: [chatCommentRelationships, newChatMessageRelationships],
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue