refactor: organize supply frontend structure
This commit is contained in:
@@ -1,21 +1,19 @@
|
||||
import type { UIMessage } from "ai";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import type {
|
||||
AgentChatMessage,
|
||||
AgentPermissionReply,
|
||||
AgentPermissionStatus,
|
||||
AgentQuestionRequest,
|
||||
AgentStreamRenderState
|
||||
} from "@/features/agent";
|
||||
import type { AgentSessionStreamEvent } from "@/features/agent/api/client";
|
||||
import {
|
||||
applyPermissionResponse,
|
||||
applyQuestionResponse,
|
||||
toTodoUpdate,
|
||||
upsertPermission,
|
||||
upsertProgress,
|
||||
upsertQuestion
|
||||
} from "@/features/agent/session-state";
|
||||
upsertQuestion,
|
||||
type AgentChatMessage,
|
||||
type AgentPermissionReply,
|
||||
type AgentPermissionStatus,
|
||||
type AgentQuestionRequest,
|
||||
type AgentSessionStreamEvent,
|
||||
type AgentStreamRenderState
|
||||
} from "@/features/agent";
|
||||
|
||||
type AgentUiDataParts = {
|
||||
progress: Record<string, unknown>;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
type PointerEvent as ReactPointerEvent
|
||||
} from "react";
|
||||
import {
|
||||
WORKBENCH_LAYOUT,
|
||||
clampAgentPanelWidth,
|
||||
getWorkbenchViewportLayout
|
||||
} from "../layout/workbench-layout";
|
||||
|
||||
export function useAgentPanelResize(viewportWidth: number) {
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const resizeLeftRef = useRef(0);
|
||||
const cleanupRef = useRef<(() => void) | null>(null);
|
||||
const [width, setWidth] = useState<number | null>(null);
|
||||
const [resizing, setResizing] = useState(false);
|
||||
|
||||
useEffect(() => () => cleanupRef.current?.(), []);
|
||||
|
||||
useEffect(() => {
|
||||
setWidth((currentWidth) =>
|
||||
currentWidth === null ? null : clampAgentPanelWidth(currentWidth, viewportWidth)
|
||||
);
|
||||
}, [viewportWidth]);
|
||||
|
||||
const resize = useCallback((clientX: number) => {
|
||||
setWidth(clampAgentPanelWidth(clientX - resizeLeftRef.current, window.innerWidth));
|
||||
}, []);
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (event.button !== 0 || !panelRef.current) return;
|
||||
|
||||
event.preventDefault();
|
||||
resizeLeftRef.current = panelRef.current.getBoundingClientRect().left;
|
||||
setResizing(true);
|
||||
resize(event.clientX);
|
||||
|
||||
cleanupRef.current?.();
|
||||
const handlePointerMove = (pointerEvent: PointerEvent) => {
|
||||
pointerEvent.preventDefault();
|
||||
resize(pointerEvent.clientX);
|
||||
};
|
||||
const stopResizing = () => {
|
||||
cleanupRef.current?.();
|
||||
setResizing(false);
|
||||
};
|
||||
const cleanup = () => {
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", stopResizing);
|
||||
window.removeEventListener("pointercancel", stopResizing);
|
||||
cleanupRef.current = null;
|
||||
};
|
||||
|
||||
cleanupRef.current = cleanup;
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", stopResizing);
|
||||
window.addEventListener("pointercancel", stopResizing);
|
||||
},
|
||||
[resize]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback((event: ReactKeyboardEvent<HTMLDivElement>) => {
|
||||
const currentWidth =
|
||||
panelRef.current?.getBoundingClientRect().width ?? WORKBENCH_LAYOUT.desktop.agentWidth;
|
||||
let nextWidth = currentWidth;
|
||||
|
||||
if (event.key === "ArrowLeft") {
|
||||
nextWidth -= 16;
|
||||
} else if (event.key === "ArrowRight") {
|
||||
nextWidth += 16;
|
||||
} else if (event.key === "Home") {
|
||||
nextWidth = getWorkbenchViewportLayout(window.innerWidth).agentWidth;
|
||||
} else if (event.key === "End") {
|
||||
nextWidth = window.innerWidth * WORKBENCH_LAYOUT.maxAgentViewportRatio;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
setWidth(clampAgentPanelWidth(nextWidth, window.innerWidth));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleKeyDown,
|
||||
handlePointerDown,
|
||||
panelRef,
|
||||
resizing,
|
||||
width
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap, MapLayerMouseEvent } from "maplibre-gl";
|
||||
import type { RefObject } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import type { RefObject } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
@@ -7,34 +5,28 @@ import useSWR from "swr";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import type { PersonaState } from "@/shared/ai-elements/persona";
|
||||
import { env } from "@/shared/config/env";
|
||||
import { showMapNotice } from "@/features/map/core/components/notice-actions";
|
||||
import type {
|
||||
AgentApprovalMode,
|
||||
AgentChatMessage,
|
||||
AgentModelOption,
|
||||
AgentPermissionReply,
|
||||
AgentPermissionRequest,
|
||||
AgentQuestionRequest,
|
||||
AgentStreamRenderState
|
||||
} from "@/features/agent";
|
||||
import {
|
||||
createAgentApiClient,
|
||||
type AgentSessionStreamEvent
|
||||
} from "@/features/agent/api/client";
|
||||
import { showMapNotice } from "@/features/map/core";
|
||||
import {
|
||||
FrontendActionExecutor,
|
||||
agentBootstrapKey,
|
||||
agentSessionsKey,
|
||||
createAgentApiClient,
|
||||
fetchAgentBootstrap,
|
||||
fetchAgentSessions
|
||||
} from "@/features/agent/api/swr";
|
||||
import {
|
||||
fetchAgentSessions,
|
||||
isUiEnvelopeAllowed,
|
||||
parseUiEnvelopePayload,
|
||||
type AgentApprovalMode,
|
||||
type AgentChatMessage,
|
||||
type AgentModelOption,
|
||||
type AgentPermissionReply,
|
||||
type AgentPermissionRequest,
|
||||
type AgentQuestionRequest,
|
||||
type AgentSessionStreamEvent,
|
||||
type AgentStreamRenderState,
|
||||
type FrontendActionRequest,
|
||||
type UIEnvelopePayload,
|
||||
type UIRegistry
|
||||
} from "@/features/agent/ui-envelope";
|
||||
import type { FrontendActionRequest } from "@/features/agent/frontend-action";
|
||||
import { FrontendActionExecutor } from "@/features/agent/frontend-action/executor";
|
||||
} from "@/features/agent";
|
||||
import {
|
||||
applySessionStreamEvent,
|
||||
createCompletedStreamRenderState,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap, MapMouseEvent } from "maplibre-gl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { useEffect, useRef, useSyncExternalStore, type RefObject } from "react";
|
||||
import { getResponsiveWorkbenchPadding } from "../map/camera";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
import maplibregl, { type Map as MapLibreMap, type MapSourceDataEvent } from "maplibre-gl";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { Map as MapLibreMap, MapGeoJSONFeature, MapMouseEvent } from "maplibre-gl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formatHeaderDataTime,
|
||||
getMsUntilNextHeaderDataTimeTick
|
||||
} from "./use-workbench-runtime-data";
|
||||
|
||||
describe("workbench runtime data", () => {
|
||||
it("snaps the header time to the current five-minute interval", () => {
|
||||
expect(formatHeaderDataTime(new Date(2026, 6, 22, 15, 43, 31))).toBe("2026-07-22 15:40");
|
||||
});
|
||||
|
||||
it("schedules the next header refresh on the next interval boundary", () => {
|
||||
expect(getMsUntilNextHeaderDataTimeTick(new Date(2026, 6, 22, 15, 43, 30))).toBe(90_000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
SCHEDULED_CONDITION_REFRESH_INTERVAL_MS,
|
||||
createOperationalScheduledConditions
|
||||
} from "../data/scheduled-conditions";
|
||||
import type { ScheduledConditionItem } from "../types";
|
||||
|
||||
const HEADER_DATA_TIME_STEP_MINUTES = 5;
|
||||
const INITIAL_HEADER_DATA_TIME = "同步中";
|
||||
|
||||
export function formatHeaderDataTime(date: Date) {
|
||||
const snappedMinutes =
|
||||
Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES;
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(snappedMinutes).padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
export function getMsUntilNextHeaderDataTimeTick(date: Date) {
|
||||
const nextTick = new Date(date);
|
||||
const nextMinutes =
|
||||
Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES +
|
||||
HEADER_DATA_TIME_STEP_MINUTES;
|
||||
|
||||
nextTick.setMinutes(nextMinutes, 0, 0);
|
||||
return Math.max(nextTick.getTime() - date.getTime(), 1_000);
|
||||
}
|
||||
|
||||
export function useWorkbenchRuntimeData() {
|
||||
const [headerDataTime, setHeaderDataTime] = useState(INITIAL_HEADER_DATA_TIME);
|
||||
const [scheduledConditions, setScheduledConditions] = useState<ScheduledConditionItem[]>([]);
|
||||
const [scheduledConditionsLoading, setScheduledConditionsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: number;
|
||||
|
||||
const refreshHeaderDataTime = () => {
|
||||
const now = new Date();
|
||||
setHeaderDataTime(formatHeaderDataTime(now));
|
||||
timeoutId = window.setTimeout(refreshHeaderDataTime, getMsUntilNextHeaderDataTimeTick(now));
|
||||
};
|
||||
|
||||
refreshHeaderDataTime();
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshScheduledConditions = () => {
|
||||
setScheduledConditions(createOperationalScheduledConditions(new Date()));
|
||||
setScheduledConditionsLoading(false);
|
||||
};
|
||||
|
||||
refreshScheduledConditions();
|
||||
const intervalId = window.setInterval(
|
||||
refreshScheduledConditions,
|
||||
SCHEDULED_CONDITION_REFRESH_INTERVAL_MS
|
||||
);
|
||||
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
headerDataTime,
|
||||
scheduledConditions,
|
||||
scheduledConditionsLoading
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user