refactor: organize supply frontend structure
This commit is contained in:
@@ -1,37 +1,169 @@
|
|||||||
# Repository Guidelines
|
# Repository Guidelines
|
||||||
|
|
||||||
## Project Structure
|
## Scope
|
||||||
|
|
||||||
This is a Vite React single-page WebGIS Agent workbench. Runtime TypeScript lives
|
This repository contains the Vite and React single-page frontend for the
|
||||||
under `src/`.
|
Agent-driven supply-network WebGIS. Runtime TypeScript lives under `src/`.
|
||||||
|
The application uses React 19, TypeScript, Tailwind CSS, MapLibre GL, deck.gl,
|
||||||
|
SWR, Vitest, and Playwright.
|
||||||
|
|
||||||
- `src/app/`: application shell and providers.
|
Keep this project browser-first. Do not add Next.js APIs, `"use client"`
|
||||||
- `src/features/agent/`: controlled Agent UI protocol, schemas, renderer, and views.
|
directives, or general backend-for-frontend routes. Agent reasoning and tool
|
||||||
- `src/features/map/`: WebGIS map shell and future MapLibre/deck.gl integrations.
|
execution belong in `next-tjwater-agent`.
|
||||||
- `src/shared/`: reusable UI, config, styles, and utilities.
|
|
||||||
- `src/mocks/`: MSW handlers and fixtures for backend-independent development.
|
|
||||||
- `src/test/`: Vitest setup.
|
|
||||||
- `server/`: same-origin Edge TTS adapter used by Vite and the production container.
|
|
||||||
|
|
||||||
## Commands
|
## Source Layout
|
||||||
|
|
||||||
Use `pnpm`.
|
- `src/app/` owns application startup, providers, and shell composition.
|
||||||
|
- `src/features/workbench/` owns supply sources, layers, SCADA overlays,
|
||||||
|
scenarios, feature adapters, GeoServer configuration, and workbench
|
||||||
|
interactions.
|
||||||
|
- `src/features/agent/` owns Agent protocol clients, sessions, typed frontend
|
||||||
|
actions, validated UI envelopes, recommendations, and Agent panels.
|
||||||
|
- `src/features/map/core/` owns domain-neutral map controls, legends, notices,
|
||||||
|
and reusable map presentation primitives.
|
||||||
|
- `src/shared/` owns reusable UI, AI presentation elements, runtime
|
||||||
|
configuration, and feature-independent utilities.
|
||||||
|
- `src/mocks/` owns MSW handlers and fixtures.
|
||||||
|
- `src/test/` owns shared Vitest setup and repository-wide invariant tests.
|
||||||
|
- `tests/browser/` owns product-level Playwright regressions.
|
||||||
|
- `public/` contains static supply-network and SCADA assets plus the
|
||||||
|
runtime-config placeholder.
|
||||||
|
- `server/` contains the same-origin Edge TTS adapter used by Vite and the
|
||||||
|
production container.
|
||||||
|
|
||||||
```bash
|
Do not recreate a catch-all `src/lib/` directory. Put feature-independent
|
||||||
pnpm dev
|
helpers in `src/shared/`; put supply-network and GeoServer logic in the
|
||||||
pnpm build
|
owning `workbench` module.
|
||||||
pnpm typecheck
|
|
||||||
pnpm lint
|
Do not create empty placeholder directories. Add a directory with its first
|
||||||
pnpm test
|
source file.
|
||||||
pnpm test:browser
|
|
||||||
|
## Dependency Direction and Public APIs
|
||||||
|
|
||||||
|
Keep dependencies moving toward generic code:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app -> workbench, map/core, shared
|
||||||
|
workbench -> agent, map/core, shared
|
||||||
|
agent -> map/core, shared
|
||||||
|
map/core -> shared
|
||||||
|
shared -> external packages only
|
||||||
```
|
```
|
||||||
|
|
||||||
## Coding Notes
|
- `src/app` may import the `workbench` and `map/core` public APIs plus
|
||||||
|
shared startup configuration.
|
||||||
|
- `workbench` may import `agent`, `map/core`, and `shared`.
|
||||||
|
- `agent` may import generic `map/core` presentation helpers and `shared`,
|
||||||
|
but it must not import `workbench`.
|
||||||
|
- `map/core` must not know supply layer IDs, SCADA models, workbench state, or
|
||||||
|
Agent workflows.
|
||||||
|
- `shared` must not import from `app`, `features`, or `mocks`.
|
||||||
|
- Runtime feature code must not import from `app` or `mocks`.
|
||||||
|
|
||||||
Use TypeScript and React function components. Prefer explicit exported types at
|
Use a feature's `index.ts` as its public API for cross-feature imports. Add an
|
||||||
module boundaries. Keep Agent dynamic UI rendering schema-driven: Agent output
|
intentional public export when another feature needs a symbol. Use relative
|
||||||
must be validated before it reaches React components.
|
imports within the same feature. Do not introduce cross-feature deep imports
|
||||||
|
or barrels for private implementation files.
|
||||||
|
|
||||||
Do not add Next.js APIs or general BFF routes to this frontend. Agent behavior
|
## Component and Module Design
|
||||||
belongs in the Agent service; the local `server/` exception is limited to the
|
|
||||||
same-origin Edge TTS network adapter.
|
- Use TypeScript and React function components. Export explicit types at
|
||||||
|
feature and adapter boundaries.
|
||||||
|
- Use two-space indentation and kebab-case filenames. Use PascalCase for
|
||||||
|
components and types, camelCase for functions and values, and `useXxx` for
|
||||||
|
hooks.
|
||||||
|
- Keep application and page components focused on composition. Extract state,
|
||||||
|
effects, network flows, resize behavior, and map orchestration into focused
|
||||||
|
hooks, controllers, or child components.
|
||||||
|
- Keep MapLibre sources, layers, camera helpers, feature queries, and event
|
||||||
|
handling outside presentational components. Map mutations belong in typed
|
||||||
|
controllers or map helpers.
|
||||||
|
- Keep Agent rendering schema-driven. Validate Agent output before it reaches
|
||||||
|
React. Model map actions as typed data that can be previewed, confirmed,
|
||||||
|
applied, and rolled back.
|
||||||
|
- Keep reusable UI free of supply-network terminology and workflow state. The
|
||||||
|
owning feature decides business behavior.
|
||||||
|
- Prefer one source of truth. Search for an existing utility, config wrapper,
|
||||||
|
style token, or domain type before adding another.
|
||||||
|
|
||||||
|
## Supply-Domain Boundary
|
||||||
|
|
||||||
|
This repository models a supply network. Pipes, junctions, valves, reservoirs,
|
||||||
|
tanks, pumps, pressure, flow, water age, demand, isolation, and continuity of
|
||||||
|
supply belong in `workbench`.
|
||||||
|
|
||||||
|
Do not introduce drainage-network layer IDs, assets, terminology, export
|
||||||
|
filenames, configuration prefixes, fixtures, or operating assumptions.
|
||||||
|
Drainage concepts such as conduits, orifices, outfalls, manholes, sewage,
|
||||||
|
stormwater, and overflow risk do not belong here.
|
||||||
|
|
||||||
|
`next-tjwater-drainage-frontend` may be consulted for generic architecture
|
||||||
|
patterns only. Never copy its domain constants, source catalogs, map styles,
|
||||||
|
icons, mocks, product copy, environment variables, or defaults into this
|
||||||
|
repository. The supply-domain invariant test in
|
||||||
|
`src/test/supply-domain.test.ts` must remain green.
|
||||||
|
|
||||||
|
## Runtime and Server Boundaries
|
||||||
|
|
||||||
|
Browser configuration is injected through `/runtime-config.js` and validated
|
||||||
|
in `src/shared/config/env.ts`. Use `TJWATER_*` variables. Do not add
|
||||||
|
`DRAINAGE_*`, `VITE_*`, or legacy `NEXT_PUBLIC_*` browser configuration.
|
||||||
|
|
||||||
|
Browser-safe GeoServer reads may happen directly when CORS is enabled. Agent
|
||||||
|
requests go to the configured Agent service. The local `server/` exception is
|
||||||
|
limited to the same-origin Edge TTS network adapter; do not turn it into a
|
||||||
|
general API layer or add free-form proxy routes.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Co-locate focused Vitest tests with the module under test using `*.test.ts`
|
||||||
|
or `*.test.tsx`.
|
||||||
|
- Put repository-wide invariants and shared setup in `src/test/`.
|
||||||
|
- Put new product and interaction Playwright tests in `tests/browser/`.
|
||||||
|
`src/app/app.e2e.ts` is retained migration coverage, not a location to copy.
|
||||||
|
- Name tests after behavior. Cover pure map calculations, adapters, protocol
|
||||||
|
validation, and state transitions without requiring a browser when possible.
|
||||||
|
- Add focused Playwright coverage for visible behavior, responsive layout,
|
||||||
|
runtime configuration, keyboard and pointer interaction, or map integration.
|
||||||
|
|
||||||
|
## Commands and Verification
|
||||||
|
|
||||||
|
Use `pnpm` from this repository.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev # start Vite on http://127.0.0.1:5173
|
||||||
|
pnpm lint # run ESLint
|
||||||
|
pnpm typecheck # run TypeScript without emitting files
|
||||||
|
pnpm test # run Vitest
|
||||||
|
pnpm test:browser # run Playwright
|
||||||
|
pnpm build # typecheck, build the SPA, and bundle the TTS adapter
|
||||||
|
```
|
||||||
|
|
||||||
|
Match verification to the change:
|
||||||
|
|
||||||
|
- Documentation-only changes: run `git diff --check` and verify referenced
|
||||||
|
paths and commands.
|
||||||
|
- TypeScript logic or component changes: run `pnpm lint`, `pnpm typecheck`,
|
||||||
|
and `pnpm test`.
|
||||||
|
- Visible UI, responsive, configuration, or map interaction changes: also run
|
||||||
|
focused `pnpm test:browser` coverage.
|
||||||
|
- Dependency, Vite, container, runtime integration, or release changes: also
|
||||||
|
run `pnpm build`.
|
||||||
|
|
||||||
|
Do not report a check as passing unless it ran in the current worktree. If a
|
||||||
|
required browser or external service is unavailable, report the skipped layer
|
||||||
|
and reason.
|
||||||
|
|
||||||
|
## Security and Repository Hygiene
|
||||||
|
|
||||||
|
Do not commit secrets, `.env.local`, generated `dist/` or `dist-server/`,
|
||||||
|
dependency folders, Playwright artifacts, session dumps, tokens, or logs. Keep
|
||||||
|
server-only targets and credentials in untracked environment files.
|
||||||
|
|
||||||
|
Treat Agent output, UI envelopes, feature identifiers, URLs, and runtime
|
||||||
|
configuration as untrusted input. Validate at system boundaries, keep
|
||||||
|
credentials out of browser-visible values, and use typed adapters instead of
|
||||||
|
free-form command or request construction.
|
||||||
|
|
||||||
|
Preserve unrelated work in a dirty checkout. Do not clean, reset, move, or
|
||||||
|
rewrite user changes while completing a scoped task.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "tjwater-webgis-frontend",
|
"name": "next-tjwater-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ test("renders map notices through a single toaster", async ({ page }) => {
|
|||||||
await page.getByLabel("打开用户菜单").click();
|
await page.getByLabel("打开用户菜单").click();
|
||||||
await page.getByRole("menuitem", { name: /查看运行状态/ }).click();
|
await page.getByRole("menuitem", { name: /查看运行状态/ }).click();
|
||||||
|
|
||||||
await expect(page.locator("[data-sonner-toast]")).toHaveCount(1);
|
await expect(page.getByText("数据状态", { exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("preserves compact typography in header and agent controls", async ({ page }) => {
|
test("preserves compact typography in header and agent controls", async ({ page }) => {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { MapToaster } from "@/features/map/core/components/notice";
|
import { MapToaster } from "@/features/map/core";
|
||||||
import { MapWorkbenchPage } from "@/features/workbench";
|
import { MapWorkbenchPage } from "@/features/workbench";
|
||||||
import { AppProviders } from "./providers";
|
import { AppProviders } from "./providers";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import type { PersonaState } from "@/shared/ai-elements/persona";
|
import type { PersonaState } from "@/shared/ai-elements/persona";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { MAP_MAJOR_PANEL_RADIUS_CLASS_NAME } from "@/features/map/core/components/map-control-styles";
|
import { MAP_MAJOR_PANEL_RADIUS_CLASS_NAME } from "@/features/map/core";
|
||||||
import { AgentPersona } from "./agent-persona";
|
import { AgentPersona } from "./agent-persona";
|
||||||
|
|
||||||
type AgentCollapsedRailProps = {
|
type AgentCollapsedRailProps = {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -23,7 +21,7 @@ import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "motion/
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useStickToBottomContext } from "use-stick-to-bottom";
|
import { useStickToBottomContext } from "use-stick-to-bottom";
|
||||||
import { showMapNotice } from "@/features/map/core/components/notice-actions";
|
import { showMapNotice } from "@/features/map/core";
|
||||||
import { Agent, AgentContent } from "@/shared/ai-elements/agent";
|
import { Agent, AgentContent } from "@/shared/ai-elements/agent";
|
||||||
import {
|
import {
|
||||||
Conversation,
|
Conversation,
|
||||||
@@ -48,7 +46,7 @@ import {
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from "@/shared/ui/dropdown-menu";
|
} from "@/shared/ui/dropdown-menu";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type { AgentChatSessionSummary } from "../api/client";
|
import type { AgentChatSessionSummary } from "../api/client";
|
||||||
import { AgentPersona } from "./agent-persona";
|
import { AgentPersona } from "./agent-persona";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Check, Loader2, Pencil, RefreshCw, Trash2, X } from "lucide-react";
|
import { Check, Loader2, Pencil, RefreshCw, Trash2, X } from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@/shared/ui/input";
|
import { Input } from "@/shared/ui/input";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type { AgentChatSessionSummary } from "../api/client";
|
import type { AgentChatSessionSummary } from "../api/client";
|
||||||
import {
|
import {
|
||||||
agentLayoutTransition,
|
agentLayoutTransition,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -13,7 +11,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import {
|
import {
|
||||||
agentEase,
|
agentEase,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { Variants } from "motion/react";
|
import type { Variants } from "motion/react";
|
||||||
|
|
||||||
export const AGENT_MOTION_DURATION_MS = 180;
|
export const AGENT_MOTION_DURATION_MS = 180;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
Crosshair,
|
Crosshair,
|
||||||
@@ -8,7 +6,7 @@ import {
|
|||||||
Route
|
Route
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type {
|
import type {
|
||||||
AgentChatMessage
|
AgentChatMessage
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Bot } from "lucide-react";
|
import { Bot } from "lucide-react";
|
||||||
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
|
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import type { PersonaState } from "@/shared/ai-elements/persona";
|
import type { PersonaState } from "@/shared/ai-elements/persona";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
|
|
||||||
const LazyPersona = lazy(async () => {
|
const LazyPersona = lazy(async () => {
|
||||||
const module = await import("@/shared/ai-elements/persona");
|
const module = await import("@/shared/ai-elements/persona");
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Activity, BarChart3, Database, History, LineChart, MonitorCog } from "lucide-react";
|
import { Activity, BarChart3, Database, History, LineChart, MonitorCog } from "lucide-react";
|
||||||
import { AnimatePresence, MotionConfig, motion } from "motion/react";
|
import { AnimatePresence, MotionConfig, motion } from "motion/react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { normalizeSafeChartData, parseChartSpec, type SafeChartData, type SafeChartSeries, type SafeChartSpec } from "../chart-data";
|
import { normalizeSafeChartData, parseChartSpec, type SafeChartData, type SafeChartSeries, type SafeChartSpec } from "../chart-data";
|
||||||
import type { AgentUiResult } from "../types";
|
import type { AgentUiResult } from "../types";
|
||||||
import type { UIEnvelope } from "../ui-envelope";
|
import type { UIEnvelope } from "../ui-envelope";
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
"use client";
|
import { cn } from "@/shared/ui/cn";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { MessageResponse } from "@/shared/ai-elements/message";
|
import { MessageResponse } from "@/shared/ai-elements/message";
|
||||||
|
|
||||||
type StreamingTokenResponseProps = {
|
type StreamingTokenResponseProps = {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
||||||
import {
|
import {
|
||||||
splitSpeechTextIntoChunks,
|
splitSpeechTextIntoChunks,
|
||||||
|
|||||||
@@ -1,15 +1,56 @@
|
|||||||
export { AgentCollapsedRail } from "./components/agent-collapsed-rail";
|
export { AgentCollapsedRail } from "./components/agent-collapsed-rail";
|
||||||
export { AgentCommandPanel } from "./components/agent-command-panel";
|
export { AgentCommandPanel } from "./components/agent-command-panel";
|
||||||
export { AgentPersona } from "./components/agent-persona";
|
export { AgentPersona } from "./components/agent-persona";
|
||||||
|
export { createAgentApiClient } from "./api/client";
|
||||||
|
export type {
|
||||||
|
AgentApiClient,
|
||||||
|
AgentChatSessionSummary,
|
||||||
|
AgentLoadedChatSession,
|
||||||
|
AgentSessionStreamEvent
|
||||||
|
} from "./api/client";
|
||||||
|
export {
|
||||||
|
agentBootstrapKey,
|
||||||
|
agentSessionsKey,
|
||||||
|
fetchAgentBootstrap,
|
||||||
|
fetchAgentSessions,
|
||||||
|
type AgentBootstrapData
|
||||||
|
} from "./api/swr";
|
||||||
export { normalizeSafeChartData, parseChartSpec, pointToLabelValue } from "./chart-data";
|
export { normalizeSafeChartData, parseChartSpec, pointToLabelValue } from "./chart-data";
|
||||||
|
export {
|
||||||
|
FRONTEND_ACTION_NAMES,
|
||||||
|
parseFrontendActionRegistry,
|
||||||
|
parseFrontendActionRequest,
|
||||||
|
readActionResults,
|
||||||
|
storeActionResult,
|
||||||
|
type FrontendActionName,
|
||||||
|
type FrontendActionRegistry,
|
||||||
|
type FrontendActionRequest,
|
||||||
|
type FrontendActionResult
|
||||||
|
} from "./frontend-action";
|
||||||
|
export { FrontendActionExecutor } from "./frontend-action/executor";
|
||||||
export {
|
export {
|
||||||
applyPermissionResponse,
|
applyPermissionResponse,
|
||||||
applyQuestionResponse,
|
applyQuestionResponse,
|
||||||
|
cancelRunningTodos,
|
||||||
|
completeRunningProgress,
|
||||||
|
finalizeAssistantMessageAfterAbort,
|
||||||
toTodoUpdate,
|
toTodoUpdate,
|
||||||
upsertPermission,
|
upsertPermission,
|
||||||
upsertProgress,
|
upsertProgress,
|
||||||
upsertQuestion
|
upsertQuestion
|
||||||
} from "./session-state";
|
} from "./session-state";
|
||||||
|
export {
|
||||||
|
isUiEnvelopeAllowed,
|
||||||
|
parseUiEnvelope,
|
||||||
|
parseUiEnvelopePayload,
|
||||||
|
parseUiRegistry,
|
||||||
|
toTrustedMapAction,
|
||||||
|
type TrustedMapAction,
|
||||||
|
type UIEnvelope,
|
||||||
|
type UIEnvelopePayload,
|
||||||
|
type UIRegistry,
|
||||||
|
type UISurface
|
||||||
|
} from "./ui-envelope";
|
||||||
export type {
|
export type {
|
||||||
AgentApprovalMode,
|
AgentApprovalMode,
|
||||||
AgentChatMessage,
|
AgentChatMessage,
|
||||||
@@ -28,8 +69,4 @@ export type {
|
|||||||
AgentTodoUpdate,
|
AgentTodoUpdate,
|
||||||
AgentUiResult
|
AgentUiResult
|
||||||
} from "./types";
|
} from "./types";
|
||||||
export type {
|
export type { SafeChartData, SafeChartSeries, SafeChartSpec } from "./chart-data";
|
||||||
SafeChartData,
|
|
||||||
SafeChartSeries,
|
|
||||||
SafeChartSpec
|
|
||||||
} from "./chart-data";
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Download, ListChecks, MapPinned, Plus, Share2 } from "lucide-react";
|
import { Download, ListChecks, MapPinned, Plus, Share2 } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
MapActionRow,
|
MapActionRow,
|
||||||
MapPanelSection
|
MapPanelSection
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Check, Map } from "lucide-react";
|
import { Check, Map } from "lucide-react";
|
||||||
import { type FocusEvent, useCallback, useEffect, useRef, useState } from "react";
|
import { type FocusEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
MAP_CONTROL_RADIUS_CLASS_NAME,
|
MAP_CONTROL_RADIUS_CLASS_NAME,
|
||||||
MAP_CONTROL_SURFACE_CLASS_NAME,
|
MAP_CONTROL_SURFACE_CLASS_NAME,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ChevronRight, type LucideIcon } from "lucide-react";
|
import { ChevronRight, type LucideIcon } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
MAP_COMPACT_RADIUS_CLASS_NAME,
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
||||||
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Eye, EyeOff, Layers3, Lock } from "lucide-react";
|
import { Eye, EyeOff, Layers3, Lock } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
MAP_COMPACT_RADIUS_CLASS_NAME,
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
||||||
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Check, Eye, EyeOff, Lock, Map } from "lucide-react";
|
import { Check, Eye, EyeOff, Lock, Map } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type { BaseLayerOption } from "./base-layers-control";
|
import type { BaseLayerOption } from "./base-layers-control";
|
||||||
import type { MapLayerControlItem } from "./layer-control";
|
import type { MapLayerControlItem } from "./layer-control";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
||||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Copy, Ruler, Trash2 } from "lucide-react";
|
import { Copy, Ruler, Trash2 } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
MapActionRow,
|
MapActionRow,
|
||||||
MapModeButton,
|
MapModeButton,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { toast, Toaster } from "sonner";
|
import { toast, Toaster } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { MAP_ICON_CELL_RADIUS_CLASS_NAME } from "./map-control-styles";
|
import { MAP_ICON_CELL_RADIUS_CLASS_NAME } from "./map-control-styles";
|
||||||
import { MapNoticeCloseIcon, MapNoticeContent } from "./notice-presentation";
|
import { MapNoticeCloseIcon, MapNoticeContent } from "./notice-presentation";
|
||||||
import type { MapNoticeOptions, MapNoticePosition } from "./notice-types";
|
import type { MapNoticeOptions, MapNoticePosition } from "./notice-types";
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { AlertTriangle, CheckCircle2, Info, Loader2, X, XCircle } from "lucide-react";
|
import { AlertTriangle, CheckCircle2, Info, Loader2, X, XCircle } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
MAP_COMPACT_RADIUS_CLASS_NAME,
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
||||||
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import { dismissMapNotice, showMapNotice } from "./notice-actions";
|
import { dismissMapNotice, showMapNotice } from "./notice-actions";
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||||
import { type RefObject, useEffect, useState } from "react";
|
import { type RefObject, useEffect, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { MAP_CONTROL_SURFACE_CLASS_NAME } from "./map-control-styles";
|
import { MAP_CONTROL_SURFACE_CLASS_NAME } from "./map-control-styles";
|
||||||
|
|
||||||
type ScaleLineState = {
|
type ScaleLineState = {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||||
import { Home, Minus, Plus, type LucideIcon } from "lucide-react";
|
import { Home, Minus, Plus, type LucideIcon } from "lucide-react";
|
||||||
import { type RefObject, useCallback } from "react";
|
import { type RefObject, useCallback } from "react";
|
||||||
@@ -9,7 +7,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from "@/shared/ui/tooltip";
|
} from "@/shared/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
MAP_CONTROL_HOVER_CLASS_NAME,
|
MAP_CONTROL_HOVER_CLASS_NAME,
|
||||||
MAP_CONTROL_RADIUS_CLASS_NAME,
|
MAP_CONTROL_RADIUS_CLASS_NAME,
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
export {
|
||||||
|
MapAnnotationPanel,
|
||||||
|
type MapAnnotationItem,
|
||||||
|
type MapAnnotationPanelProps,
|
||||||
|
type MapAnnotationShareAction
|
||||||
|
} from "./components/annotation-panel";
|
||||||
|
export { BaseLayersControl, type BaseLayerOption } from "./components/base-layers-control";
|
||||||
|
export {
|
||||||
|
MapActionChip,
|
||||||
|
MapActionRow,
|
||||||
|
MapControlPanel,
|
||||||
|
MapMetricTile,
|
||||||
|
MapModeButton,
|
||||||
|
MapPanelSection,
|
||||||
|
type MapActionRowProps,
|
||||||
|
type MapControlPanelProps,
|
||||||
|
type MapModeButtonProps
|
||||||
|
} from "./components/control-panel";
|
||||||
|
export {
|
||||||
|
MapDrawToolbar,
|
||||||
|
type MapDrawToolbarProps,
|
||||||
|
type MapDrawTool
|
||||||
|
} from "./components/draw-toolbar";
|
||||||
|
export { MapLayerControl, type MapLayerControlItem } from "./components/layer-control";
|
||||||
|
export { MapLayerPanel, type MapLayerPanelProps } from "./components/layer-panel";
|
||||||
|
export { MapLegend, MapLegendList, MapLegendSwatch, type MapLegendItem } from "./components/legend";
|
||||||
|
export {
|
||||||
|
MAP_COMPACT_RADIUS_CLASS_NAME,
|
||||||
|
MAP_CONTROL_HOVER_CLASS_NAME,
|
||||||
|
MAP_CONTROL_RADIUS_CLASS_NAME,
|
||||||
|
MAP_CONTROL_SURFACE_CLASS_NAME,
|
||||||
|
MAP_FOCUS_SURFACE_CLASS_NAME,
|
||||||
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
||||||
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
||||||
|
MAP_READABLE_RADIUS_CLASS_NAME,
|
||||||
|
MAP_READABLE_SURFACE_CLASS_NAME,
|
||||||
|
MAP_READABLE_SURFACE_STRONG_CLASS_NAME,
|
||||||
|
MAP_TOOL_PANEL_SURFACE_CLASS_NAME
|
||||||
|
} from "./components/map-control-styles";
|
||||||
|
export {
|
||||||
|
MapMeasurePanel,
|
||||||
|
type MapMeasureAction,
|
||||||
|
type MapMeasureMode,
|
||||||
|
type MapMeasurePanelProps,
|
||||||
|
type MapMeasureResultMetric,
|
||||||
|
type MapMeasureUnit
|
||||||
|
} from "./components/measure-panel";
|
||||||
|
export { dismissMapNotice, showMapNotice } from "./components/notice-actions";
|
||||||
|
export {
|
||||||
|
MapErrorNotice,
|
||||||
|
MapLoadingNotice,
|
||||||
|
MapSourceStatusNotice,
|
||||||
|
MapToaster,
|
||||||
|
type MapNoticeOptions,
|
||||||
|
type MapNoticePosition,
|
||||||
|
type MapNoticeTone,
|
||||||
|
type MapSourceStatus,
|
||||||
|
type MapSourceStatusNoticeProps
|
||||||
|
} from "./components/notice";
|
||||||
|
export { MapScaleLine } from "./components/scale-line";
|
||||||
|
export { MapToolbar, type MapToolbarItem } from "./components/toolbar";
|
||||||
|
export { MapZoom } from "./components/zoom";
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { AnimatePresence, motion, type Variants } from "motion/react";
|
import { AnimatePresence, motion, type Variants } from "motion/react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type {
|
import type {
|
||||||
ScheduledConditionRecord
|
ScheduledConditionRecord
|
||||||
} from "@/features/workbench/types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
createVisibleTickerCards,
|
createVisibleTickerCards,
|
||||||
type TickerStackCard
|
type TickerStackCard
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ScheduledConditionRiskLevel,
|
ScheduledConditionRiskLevel,
|
||||||
ScheduledConditionStatus
|
ScheduledConditionStatus
|
||||||
} from "@/features/workbench/types";
|
} from "../types";
|
||||||
|
|
||||||
export function getConditionReportRiskLevel(
|
export function getConditionReportRiskLevel(
|
||||||
status: ScheduledConditionStatus,
|
status: ScheduledConditionStatus,
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import type { ReactNode, SVGProps } from "react";
|
|
||||||
|
|
||||||
export type DrainageFeatureIconProps = SVGProps<SVGSVGElement> & {
|
|
||||||
size?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DrainageIconFrameProps = DrainageFeatureIconProps & {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
function DrainageIconFrame({ children, size = 20, ...props }: DrainageIconFrameProps) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.8"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
focusable="false"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConduitFeatureIcon(props: DrainageFeatureIconProps) {
|
|
||||||
return (
|
|
||||||
<DrainageIconFrame {...props}>
|
|
||||||
<circle cx="3.5" cy="12" r="1.75" />
|
|
||||||
<circle cx="20.5" cy="12" r="1.75" />
|
|
||||||
<path d="M5.25 9.5h13.5M5.25 14.5h13.5" />
|
|
||||||
</DrainageIconFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function JunctionFeatureIcon(props: DrainageFeatureIconProps) {
|
|
||||||
return (
|
|
||||||
<DrainageIconFrame {...props}>
|
|
||||||
<circle cx="12" cy="12" r="5" />
|
|
||||||
<circle cx="12" cy="12" r="1.75" />
|
|
||||||
<path d="M12 2.5V7M2.5 12H7M17 12h4.5M12 17v4.5" />
|
|
||||||
</DrainageIconFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ValveFeatureIcon(props: DrainageFeatureIconProps) {
|
|
||||||
return (
|
|
||||||
<DrainageIconFrame {...props}>
|
|
||||||
<path d="M3.5 12h4.75M15.75 12h4.75" />
|
|
||||||
<path d="m8.25 7 7.5 5-7.5 5V7Z" />
|
|
||||||
<path d="m15.75 7-7.5 5 7.5 5V7Z" />
|
|
||||||
<path d="M12 7V3.5M9.5 3.5h5" />
|
|
||||||
</DrainageIconFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ReservoirFeatureIcon(props: DrainageFeatureIconProps) {
|
|
||||||
return (
|
|
||||||
<DrainageIconFrame {...props}>
|
|
||||||
<path d="M5 8.5c0-2.2 14-2.2 14 0v7c0 2.2-14 2.2-14 0v-7Z" />
|
|
||||||
<path d="M5 8.5c0 2.2 14 2.2 14 0" />
|
|
||||||
<path d="M5 12c0 2.2 14 2.2 14 0" />
|
|
||||||
<path d="M8 19.5h8" />
|
|
||||||
</DrainageIconFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScadaFeatureIcon(props: DrainageFeatureIconProps) {
|
|
||||||
return (
|
|
||||||
<DrainageIconFrame {...props}>
|
|
||||||
<circle cx="12" cy="7.5" r="1.75" />
|
|
||||||
<path d="M8.25 4.25a5.25 5.25 0 0 0 0 6.5M15.75 4.25a5.25 5.25 0 0 1 0 6.5" />
|
|
||||||
<path d="M5.25 2.25a9 9 0 0 0 0 10.5M18.75 2.25a9 9 0 0 1 0 10.5" />
|
|
||||||
<path d="M12 9.25v5.25M3.5 18h4l1.5-3 2.75 6 2.5-5 1.25 2h5" />
|
|
||||||
</DrainageIconFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OrificeFeatureIcon(props: DrainageFeatureIconProps) {
|
|
||||||
return (
|
|
||||||
<DrainageIconFrame {...props}>
|
|
||||||
<path d="M2.5 12H9M15 12h6.5" />
|
|
||||||
<path d="M9.5 4v5.35M9.5 14.65V20M14.5 4v5.35M14.5 14.65V20" />
|
|
||||||
<circle cx="12" cy="12" r="3" />
|
|
||||||
</DrainageIconFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PumpFeatureIcon(props: DrainageFeatureIconProps) {
|
|
||||||
return (
|
|
||||||
<DrainageIconFrame {...props}>
|
|
||||||
<circle cx="9.5" cy="13.5" r="6" />
|
|
||||||
<circle cx="9.5" cy="13.5" r="1.25" />
|
|
||||||
<path d="M9.5 12.25V8.5M10.75 13.5h3.75M8.65 14.4 6.2 16.85" />
|
|
||||||
<path d="M9.5 7.5V3.5h10v5M3.5 13.5H1.75" />
|
|
||||||
<path d="m17 6 2.5 2.5L22 6" />
|
|
||||||
</DrainageIconFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OutfallFeatureIcon(props: DrainageFeatureIconProps) {
|
|
||||||
return (
|
|
||||||
<DrainageIconFrame {...props}>
|
|
||||||
<path d="M4 4.5h10v7h5" />
|
|
||||||
<path d="m16.5 8.75 2.75 2.75-2.75 2.75" />
|
|
||||||
<path d="M3 18c1.5-1.25 3-1.25 4.5 0s3 1.25 4.5 0 3-1.25 4.5 0 3 1.25 4.5 0M5 21c1.15-.75 2.3-.75 3.45 0s2.3.75 3.45 0 2.3-.75 3.45 0 2.3.75 3.45 0" />
|
|
||||||
</DrainageIconFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Target, TriangleAlert, X } from "lucide-react";
|
import { Target, TriangleAlert, X } from "lucide-react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import type { DetailFeature } from "../types";
|
import type { DetailFeature } from "../types";
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Check, Copy, Database, X } from "lucide-react";
|
import { Check, Copy, Database, X } from "lucide-react";
|
||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
MAP_FOCUS_SURFACE_CLASS_NAME,
|
MAP_FOCUS_SURFACE_CLASS_NAME,
|
||||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
||||||
} from "@/features/map/core/components/map-control-styles";
|
showMapNotice
|
||||||
import { showMapNotice } from "@/features/map/core/components/notice-actions";
|
} from "@/features/map/core";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type { WaterNetworkSourceId } from "../map/sources";
|
import type { WaterNetworkSourceId } from "../map/sources";
|
||||||
import type { DetailFeature } from "../types";
|
import type { DetailFeature } from "../types";
|
||||||
import {
|
import {
|
||||||
@@ -16,14 +14,14 @@ import {
|
|||||||
type FeaturePanelBadgeTone
|
type FeaturePanelBadgeTone
|
||||||
} from "../utils/feature-properties";
|
} from "../utils/feature-properties";
|
||||||
import {
|
import {
|
||||||
ConduitFeatureIcon,
|
PipeFeatureIcon,
|
||||||
JunctionFeatureIcon,
|
JunctionFeatureIcon,
|
||||||
PumpFeatureIcon,
|
PumpFeatureIcon,
|
||||||
ReservoirFeatureIcon,
|
ReservoirFeatureIcon,
|
||||||
ScadaFeatureIcon,
|
ScadaFeatureIcon,
|
||||||
ValveFeatureIcon,
|
ValveFeatureIcon,
|
||||||
type DrainageFeatureIconProps
|
type SupplyFeatureIconProps
|
||||||
} from "./drainage-feature-icons";
|
} from "./supply-feature-icons";
|
||||||
import { FeaturePropertyList } from "./feature-property-list";
|
import { FeaturePropertyList } from "./feature-property-list";
|
||||||
|
|
||||||
type FeaturePopoverProps = {
|
type FeaturePopoverProps = {
|
||||||
@@ -34,14 +32,14 @@ type FeaturePopoverProps = {
|
|||||||
const FEATURE_LAYER_META: Record<
|
const FEATURE_LAYER_META: Record<
|
||||||
WaterNetworkSourceId,
|
WaterNetworkSourceId,
|
||||||
{
|
{
|
||||||
icon: ComponentType<DrainageFeatureIconProps>;
|
icon: ComponentType<SupplyFeatureIconProps>;
|
||||||
label: string;
|
label: string;
|
||||||
headerClassName: string;
|
headerClassName: string;
|
||||||
labelClassName: string;
|
labelClassName: string;
|
||||||
iconClassName: string;
|
iconClassName: string;
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
pipes: { icon: ConduitFeatureIcon, label: "管线", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" },
|
pipes: { icon: PipeFeatureIcon, label: "管线", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" },
|
||||||
junctions: { icon: JunctionFeatureIcon, label: "节点", headerClassName: "bg-blue-50/70", labelClassName: "text-blue-800", iconClassName: "bg-sky-700 text-white ring-sky-900/10" },
|
junctions: { icon: JunctionFeatureIcon, label: "节点", headerClassName: "bg-blue-50/70", labelClassName: "text-blue-800", iconClassName: "bg-sky-700 text-white ring-sky-900/10" },
|
||||||
valves: { icon: ValveFeatureIcon, label: "阀门", headerClassName: "bg-violet-50/70", labelClassName: "text-violet-800", iconClassName: "bg-violet-600 text-white ring-violet-900/10" },
|
valves: { icon: ValveFeatureIcon, label: "阀门", headerClassName: "bg-violet-50/70", labelClassName: "text-violet-800", iconClassName: "bg-violet-600 text-white ring-violet-900/10" },
|
||||||
reservoirs: { icon: ReservoirFeatureIcon, label: "水库", headerClassName: "bg-sky-50/70", labelClassName: "text-sky-900", iconClassName: "bg-sky-800 text-white ring-sky-900/10" },
|
reservoirs: { icon: ReservoirFeatureIcon, label: "水库", headerClassName: "bg-sky-50/70", labelClassName: "text-sky-900", iconClassName: "bg-sky-800 text-white ring-sky-900/10" },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type { LocalizedFeatureProperty } from "../utils/feature-properties";
|
import type { LocalizedFeatureProperty } from "../utils/feature-properties";
|
||||||
|
|
||||||
type FeaturePropertyListProps = {
|
type FeaturePropertyListProps = {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { ChevronDown, Crosshair, LocateFixed, RotateCcw, X } from "lucide-react";
|
import { ChevronDown, Crosshair, LocateFixed, RotateCcw, X } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
LAYER_GROUP_GEOMETRIES,
|
LAYER_GROUP_GEOMETRIES,
|
||||||
type LayerGroupId,
|
type LayerGroupId,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Bot,
|
Bot,
|
||||||
@@ -20,13 +18,13 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { showMapNotice } from "@/features/map/core/components/notice-actions";
|
import { showMapNotice } from "@/features/map/core";
|
||||||
import {
|
import {
|
||||||
getRunningWorkflowDefinition,
|
getRunningWorkflowDefinition,
|
||||||
getRunningWorkflowState,
|
getRunningWorkflowState,
|
||||||
getWorkflowStepStatus
|
getWorkflowStepStatus
|
||||||
} from "@/features/workbench/data/running-workflows";
|
} from "../data/running-workflows";
|
||||||
import type {
|
import type {
|
||||||
ScheduledConditionAnalysisInsight,
|
ScheduledConditionAnalysisInsight,
|
||||||
ScheduledConditionItem,
|
ScheduledConditionItem,
|
||||||
@@ -34,7 +32,7 @@ import type {
|
|||||||
ScheduledConditionRecord,
|
ScheduledConditionRecord,
|
||||||
ScheduledConditionRiskLevel,
|
ScheduledConditionRiskLevel,
|
||||||
ScheduledWorkOrderItem
|
ScheduledWorkOrderItem
|
||||||
} from "@/features/workbench/types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
formatDuration,
|
formatDuration,
|
||||||
formatTime,
|
formatTime,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
ScheduledConditionStatus,
|
ScheduledConditionStatus,
|
||||||
ScheduledWorkOrderItem,
|
ScheduledWorkOrderItem,
|
||||||
ScheduledWorkOrderStage
|
ScheduledWorkOrderStage
|
||||||
} from "@/features/workbench/types";
|
} from "../types";
|
||||||
|
|
||||||
export const CONDITION_FILTER_ALL = "__all__";
|
export const CONDITION_FILTER_ALL = "__all__";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -10,12 +8,11 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { MAP_TOOL_PANEL_SURFACE_CLASS_NAME } from "@/features/map/core/components/map-control-styles";
|
import { MAP_TOOL_PANEL_SURFACE_CLASS_NAME, showMapNotice } from "@/features/map/core";
|
||||||
import { showMapNotice } from "@/features/map/core/components/notice-actions";
|
import { isGeneratedScheduledConditionSessionId } from "../data/scheduled-conditions";
|
||||||
import { isGeneratedScheduledConditionSessionId } from "@/features/workbench/data/scheduled-conditions";
|
import type { ScheduledConditionItem } from "../types";
|
||||||
import type { ScheduledConditionItem } from "@/features/workbench/types";
|
import { createConditionConversationPrompt } from "../utils/scheduled-condition-prompts";
|
||||||
import { createConditionConversationPrompt } from "@/features/workbench/utils/scheduled-condition-prompts";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import type { ReactNode, SVGProps } from "react";
|
||||||
|
|
||||||
|
export type SupplyFeatureIconProps = SVGProps<SVGSVGElement> & {
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SupplyIconFrameProps = SupplyFeatureIconProps & {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SupplyIconFrame({ children, size = 20, ...props }: SupplyIconFrameProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.8"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
focusable="false"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PipeFeatureIcon(props: SupplyFeatureIconProps) {
|
||||||
|
return (
|
||||||
|
<SupplyIconFrame {...props}>
|
||||||
|
<circle cx="3.5" cy="12" r="1.75" />
|
||||||
|
<circle cx="20.5" cy="12" r="1.75" />
|
||||||
|
<path d="M5.25 9.5h13.5M5.25 14.5h13.5" />
|
||||||
|
</SupplyIconFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JunctionFeatureIcon(props: SupplyFeatureIconProps) {
|
||||||
|
return (
|
||||||
|
<SupplyIconFrame {...props}>
|
||||||
|
<circle cx="12" cy="12" r="5" />
|
||||||
|
<circle cx="12" cy="12" r="1.75" />
|
||||||
|
<path d="M12 2.5V7M2.5 12H7M17 12h4.5M12 17v4.5" />
|
||||||
|
</SupplyIconFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ValveFeatureIcon(props: SupplyFeatureIconProps) {
|
||||||
|
return (
|
||||||
|
<SupplyIconFrame {...props}>
|
||||||
|
<path d="M3.5 12h4.75M15.75 12h4.75" />
|
||||||
|
<path d="m8.25 7 7.5 5-7.5 5V7Z" />
|
||||||
|
<path d="m15.75 7-7.5 5 7.5 5V7Z" />
|
||||||
|
<path d="M12 7V3.5M9.5 3.5h5" />
|
||||||
|
</SupplyIconFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReservoirFeatureIcon(props: SupplyFeatureIconProps) {
|
||||||
|
return (
|
||||||
|
<SupplyIconFrame {...props}>
|
||||||
|
<path d="M5 8.5c0-2.2 14-2.2 14 0v7c0 2.2-14 2.2-14 0v-7Z" />
|
||||||
|
<path d="M5 8.5c0 2.2 14 2.2 14 0" />
|
||||||
|
<path d="M5 12c0 2.2 14 2.2 14 0" />
|
||||||
|
<path d="M8 19.5h8" />
|
||||||
|
</SupplyIconFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScadaFeatureIcon(props: SupplyFeatureIconProps) {
|
||||||
|
return (
|
||||||
|
<SupplyIconFrame {...props}>
|
||||||
|
<circle cx="12" cy="7.5" r="1.75" />
|
||||||
|
<path d="M8.25 4.25a5.25 5.25 0 0 0 0 6.5M15.75 4.25a5.25 5.25 0 0 1 0 6.5" />
|
||||||
|
<path d="M5.25 2.25a9 9 0 0 0 0 10.5M18.75 2.25a9 9 0 0 1 0 10.5" />
|
||||||
|
<path d="M12 9.25v5.25M3.5 18h4l1.5-3 2.75 6 2.5-5 1.25 2h5" />
|
||||||
|
</SupplyIconFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PumpFeatureIcon(props: SupplyFeatureIconProps) {
|
||||||
|
return (
|
||||||
|
<SupplyIconFrame {...props}>
|
||||||
|
<circle cx="9.5" cy="13.5" r="6" />
|
||||||
|
<circle cx="9.5" cy="13.5" r="1.25" />
|
||||||
|
<path d="M9.5 12.25V8.5M10.75 13.5h3.75M8.65 14.4 6.2 16.85" />
|
||||||
|
<path d="M9.5 7.5V3.5h10v5M3.5 13.5H1.75" />
|
||||||
|
<path d="m17 6 2.5 2.5L22 6" />
|
||||||
|
</SupplyIconFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,21 +23,20 @@ import {
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
MapAnnotationPanel,
|
MapAnnotationPanel,
|
||||||
type MapAnnotationItem,
|
|
||||||
type MapAnnotationShareAction
|
|
||||||
} from "@/features/map/core/components/annotation-panel";
|
|
||||||
import type { BaseLayerOption } from "@/features/map/core/components/base-layers-control";
|
|
||||||
import {
|
|
||||||
MapActionRow,
|
MapActionRow,
|
||||||
MapControlPanel,
|
MapControlPanel,
|
||||||
|
MapDrawToolbar,
|
||||||
|
MapLayerPanel,
|
||||||
|
MapLegendList,
|
||||||
MapMetricTile,
|
MapMetricTile,
|
||||||
MapPanelSection
|
MapMeasurePanel,
|
||||||
} from "@/features/map/core/components/control-panel";
|
MapPanelSection,
|
||||||
import { MapDrawToolbar } from "@/features/map/core/components/draw-toolbar";
|
type BaseLayerOption,
|
||||||
import { MapLayerPanel } from "@/features/map/core/components/layer-panel";
|
type MapAnnotationItem,
|
||||||
import type { MapLayerControlItem } from "@/features/map/core/components/layer-control";
|
type MapAnnotationShareAction,
|
||||||
import { MapLegendList, type MapLegendItem } from "@/features/map/core/components/legend";
|
type MapLayerControlItem,
|
||||||
import { MapMeasurePanel } from "@/features/map/core/components/measure-panel";
|
type MapLegendItem
|
||||||
|
} from "@/features/map/core";
|
||||||
import {
|
import {
|
||||||
type WorkbenchDrawingAnnotation,
|
type WorkbenchDrawingAnnotation,
|
||||||
type WorkbenchDrawMode
|
type WorkbenchDrawMode
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { AgentCollapsedRail, AgentCommandPanel, AgentPersona } from "@/features/agent";
|
||||||
|
import {
|
||||||
|
MAP_CONTROL_SURFACE_CLASS_NAME,
|
||||||
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
|
||||||
|
} from "@/features/map/core";
|
||||||
|
import { cn } from "@/shared/ui/cn";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
import { WORKBENCH_LAYOUT, getWorkbenchViewportLayout } from "../layout/workbench-layout";
|
||||||
|
import { useAgentPanelResize } from "../hooks/use-agent-panel-resize";
|
||||||
|
|
||||||
|
type AgentPanelProps = Omit<ComponentProps<typeof AgentCommandPanel>, "collapsing" | "onCollapse">;
|
||||||
|
|
||||||
|
type WorkbenchAgentPanelsProps = {
|
||||||
|
panelProps: AgentPanelProps;
|
||||||
|
panelOpen: boolean;
|
||||||
|
panelCollapsing: boolean;
|
||||||
|
mobileOpen: boolean;
|
||||||
|
mobilePanelCollapsing: boolean;
|
||||||
|
personaState: ComponentProps<typeof AgentPersona>["state"];
|
||||||
|
statusLabel: string;
|
||||||
|
viewportWidth: number;
|
||||||
|
onCollapsePanel: () => void;
|
||||||
|
onCloseMobilePanel: () => void;
|
||||||
|
onExpandPanel: () => void;
|
||||||
|
onOpenMobilePanel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WorkbenchAgentPanels({
|
||||||
|
panelProps,
|
||||||
|
panelOpen,
|
||||||
|
panelCollapsing,
|
||||||
|
mobileOpen,
|
||||||
|
mobilePanelCollapsing,
|
||||||
|
personaState,
|
||||||
|
statusLabel,
|
||||||
|
viewportWidth,
|
||||||
|
onCollapsePanel,
|
||||||
|
onCloseMobilePanel,
|
||||||
|
onExpandPanel,
|
||||||
|
onOpenMobilePanel
|
||||||
|
}: WorkbenchAgentPanelsProps) {
|
||||||
|
const resize = useAgentPanelResize(viewportWidth);
|
||||||
|
const defaultWidth = getWorkbenchViewportLayout(viewportWidth).agentWidth;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={resize.panelRef}
|
||||||
|
className="pointer-events-none absolute bottom-4 left-3 top-24 z-20 hidden w-[var(--workbench-agent-width)] max-w-[50vw] 2xl:w-[var(--workbench-agent-width-wide)] lg:block"
|
||||||
|
style={resize.width === null ? undefined : { width: resize.width }}
|
||||||
|
>
|
||||||
|
{panelOpen ? (
|
||||||
|
<>
|
||||||
|
<AgentCommandPanel
|
||||||
|
{...panelProps}
|
||||||
|
collapsing={panelCollapsing}
|
||||||
|
onCollapse={onCollapsePanel}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-label="调整 Agent 面板宽度"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-valuemax={Math.round(viewportWidth * WORKBENCH_LAYOUT.maxAgentViewportRatio)}
|
||||||
|
aria-valuemin={defaultWidth}
|
||||||
|
aria-valuenow={Math.round(resize.width ?? defaultWidth)}
|
||||||
|
className={cn(
|
||||||
|
"group pointer-events-auto absolute -right-3 top-4 bottom-4 z-10 flex w-6 cursor-ew-resize touch-none items-center justify-center outline-hidden",
|
||||||
|
resize.resizing && "[&>span]:bg-blue-500 [&>span]:opacity-100"
|
||||||
|
)}
|
||||||
|
role="separator"
|
||||||
|
tabIndex={0}
|
||||||
|
title="拖拽调整 Agent 面板宽度"
|
||||||
|
onKeyDown={resize.handleKeyDown}
|
||||||
|
onPointerDown={resize.handlePointerDown}
|
||||||
|
>
|
||||||
|
<span className="h-14 w-1 rounded-full bg-slate-500/60 opacity-50 shadow-xs transition-[height,background-color,opacity] group-hover:h-20 group-hover:bg-blue-500 group-hover:opacity-100 group-focus-visible:h-20 group-focus-visible:bg-blue-500 group-focus-visible:opacity-100" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<AgentCollapsedRail
|
||||||
|
personaState={personaState}
|
||||||
|
statusLabel={statusLabel}
|
||||||
|
onExpand={onExpandPanel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-20 left-3 right-3 z-30 lg:hidden">
|
||||||
|
{mobileOpen ? (
|
||||||
|
<div className="h-[calc(100dvh-8.5rem)] min-h-[420px]">
|
||||||
|
<AgentCommandPanel
|
||||||
|
{...panelProps}
|
||||||
|
collapsing={mobilePanelCollapsing}
|
||||||
|
onCollapse={onCloseMobilePanel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!mobileOpen ? (
|
||||||
|
<div className="absolute bottom-20 left-3 z-30 lg:hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="打开 Agent 面板"
|
||||||
|
title="打开 Agent 面板"
|
||||||
|
onClick={onOpenMobilePanel}
|
||||||
|
className={cn(
|
||||||
|
"grid h-12 w-12 place-items-center text-violet-700",
|
||||||
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
||||||
|
MAP_CONTROL_SURFACE_CLASS_NAME
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AgentPersona className="pointer-events-none h-10 w-10" state={personaState} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
Bell,
|
Bell,
|
||||||
@@ -24,8 +22,8 @@ import {
|
|||||||
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
||||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
||||||
MAP_READABLE_RADIUS_CLASS_NAME
|
MAP_READABLE_RADIUS_CLASS_NAME
|
||||||
} from "@/features/map/core/components/map-control-styles";
|
} from "@/features/map/core";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -13,8 +11,8 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
MAP_ICON_CELL_RADIUS_CLASS_NAME
|
MAP_ICON_CELL_RADIUS_CLASS_NAME
|
||||||
} from "@/features/map/core/components/map-control-styles";
|
} from "@/features/map/core";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type { WorkbenchAlert, WorkbenchScenario, WorkbenchUser } from "../types";
|
import type { WorkbenchAlert, WorkbenchScenario, WorkbenchUser } from "../types";
|
||||||
import { AlertMenu, ScenarioMenu, UserMenu } from "./workbench-top-bar-menus";
|
import { AlertMenu, ScenarioMenu, UserMenu } from "./workbench-top-bar-menus";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
ScheduledConditionRecord,
|
ScheduledConditionRecord,
|
||||||
ScheduledConditionStatus,
|
ScheduledConditionStatus,
|
||||||
ScheduledConditionTaskId
|
ScheduledConditionTaskId
|
||||||
} from "@/features/workbench/types";
|
} from "../types";
|
||||||
|
|
||||||
export type RunningWorkflowStepDefinition = {
|
export type RunningWorkflowStepDefinition = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import type { UIMessage } from "ai";
|
import type { UIMessage } from "ai";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
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 {
|
import {
|
||||||
applyPermissionResponse,
|
applyPermissionResponse,
|
||||||
applyQuestionResponse,
|
applyQuestionResponse,
|
||||||
toTodoUpdate,
|
toTodoUpdate,
|
||||||
upsertPermission,
|
upsertPermission,
|
||||||
upsertProgress,
|
upsertProgress,
|
||||||
upsertQuestion
|
upsertQuestion,
|
||||||
} from "@/features/agent/session-state";
|
type AgentChatMessage,
|
||||||
|
type AgentPermissionReply,
|
||||||
|
type AgentPermissionStatus,
|
||||||
|
type AgentQuestionRequest,
|
||||||
|
type AgentSessionStreamEvent,
|
||||||
|
type AgentStreamRenderState
|
||||||
|
} from "@/features/agent";
|
||||||
|
|
||||||
type AgentUiDataParts = {
|
type AgentUiDataParts = {
|
||||||
progress: Record<string, unknown>;
|
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 { Map as MapLibreMap, MapLayerMouseEvent } from "maplibre-gl";
|
||||||
import type { RefObject } from "react";
|
import type { RefObject } from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||||
import type { RefObject } from "react";
|
import type { RefObject } from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useChat } from "@ai-sdk/react";
|
import { useChat } from "@ai-sdk/react";
|
||||||
import { DefaultChatTransport } from "ai";
|
import { DefaultChatTransport } from "ai";
|
||||||
@@ -7,34 +5,28 @@ import useSWR from "swr";
|
|||||||
import useSWRImmutable from "swr/immutable";
|
import useSWRImmutable from "swr/immutable";
|
||||||
import type { PersonaState } from "@/shared/ai-elements/persona";
|
import type { PersonaState } from "@/shared/ai-elements/persona";
|
||||||
import { env } from "@/shared/config/env";
|
import { env } from "@/shared/config/env";
|
||||||
import { showMapNotice } from "@/features/map/core/components/notice-actions";
|
import { showMapNotice } from "@/features/map/core";
|
||||||
import type {
|
|
||||||
AgentApprovalMode,
|
|
||||||
AgentChatMessage,
|
|
||||||
AgentModelOption,
|
|
||||||
AgentPermissionReply,
|
|
||||||
AgentPermissionRequest,
|
|
||||||
AgentQuestionRequest,
|
|
||||||
AgentStreamRenderState
|
|
||||||
} from "@/features/agent";
|
|
||||||
import {
|
|
||||||
createAgentApiClient,
|
|
||||||
type AgentSessionStreamEvent
|
|
||||||
} from "@/features/agent/api/client";
|
|
||||||
import {
|
import {
|
||||||
|
FrontendActionExecutor,
|
||||||
agentBootstrapKey,
|
agentBootstrapKey,
|
||||||
agentSessionsKey,
|
agentSessionsKey,
|
||||||
|
createAgentApiClient,
|
||||||
fetchAgentBootstrap,
|
fetchAgentBootstrap,
|
||||||
fetchAgentSessions
|
fetchAgentSessions,
|
||||||
} from "@/features/agent/api/swr";
|
|
||||||
import {
|
|
||||||
isUiEnvelopeAllowed,
|
isUiEnvelopeAllowed,
|
||||||
parseUiEnvelopePayload,
|
parseUiEnvelopePayload,
|
||||||
|
type AgentApprovalMode,
|
||||||
|
type AgentChatMessage,
|
||||||
|
type AgentModelOption,
|
||||||
|
type AgentPermissionReply,
|
||||||
|
type AgentPermissionRequest,
|
||||||
|
type AgentQuestionRequest,
|
||||||
|
type AgentSessionStreamEvent,
|
||||||
|
type AgentStreamRenderState,
|
||||||
|
type FrontendActionRequest,
|
||||||
type UIEnvelopePayload,
|
type UIEnvelopePayload,
|
||||||
type UIRegistry
|
type UIRegistry
|
||||||
} from "@/features/agent/ui-envelope";
|
} from "@/features/agent";
|
||||||
import type { FrontendActionRequest } from "@/features/agent/frontend-action";
|
|
||||||
import { FrontendActionExecutor } from "@/features/agent/frontend-action/executor";
|
|
||||||
import {
|
import {
|
||||||
applySessionStreamEvent,
|
applySessionStreamEvent,
|
||||||
createCompletedStreamRenderState,
|
createCompletedStreamRenderState,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { Map as MapLibreMap, MapMouseEvent } from "maplibre-gl";
|
import type { Map as MapLibreMap, MapMouseEvent } from "maplibre-gl";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||||
import { useEffect, useRef, useSyncExternalStore, type RefObject } from "react";
|
import { useEffect, useRef, useSyncExternalStore, type RefObject } from "react";
|
||||||
import { getResponsiveWorkbenchPadding } from "../map/camera";
|
import { getResponsiveWorkbenchPadding } from "../map/camera";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
|
||||||
import maplibregl, { type Map as MapLibreMap, type MapSourceDataEvent } from "maplibre-gl";
|
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 type { Map as MapLibreMap, MapGeoJSONFeature, MapMouseEvent } from "maplibre-gl";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
||||||
import {
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -8,39 +5,33 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
type CSSProperties,
|
type CSSProperties
|
||||||
type KeyboardEvent as ReactKeyboardEvent,
|
|
||||||
type PointerEvent as ReactPointerEvent
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||||
import {
|
import {
|
||||||
AgentCollapsedRail,
|
|
||||||
AgentCommandPanel,
|
AgentCommandPanel,
|
||||||
AgentPersona,
|
toTrustedMapAction,
|
||||||
|
type FrontendActionRequest,
|
||||||
|
type UIEnvelopePayload,
|
||||||
type AgentUiResult
|
type AgentUiResult
|
||||||
} from "@/features/agent";
|
} from "@/features/agent";
|
||||||
import { toTrustedMapAction, type UIEnvelopePayload } from "@/features/agent/ui-envelope";
|
|
||||||
import type { FrontendActionRequest } from "@/features/agent/frontend-action";
|
|
||||||
import {
|
import {
|
||||||
MapErrorNotice,
|
MapErrorNotice,
|
||||||
MapLoadingNotice,
|
MapLoadingNotice,
|
||||||
|
MapScaleLine,
|
||||||
MapSourceStatusNotice,
|
MapSourceStatusNotice,
|
||||||
|
MapToolbar,
|
||||||
|
MapZoom,
|
||||||
|
showMapNotice,
|
||||||
|
type MapToolbarItem,
|
||||||
type MapSourceStatus
|
type MapSourceStatus
|
||||||
} from "@/features/map/core/components/notice";
|
} from "@/features/map/core";
|
||||||
import { showMapNotice } from "@/features/map/core/components/notice-actions";
|
|
||||||
import { MapScaleLine } from "@/features/map/core/components/scale-line";
|
|
||||||
import { MapToolbar, type MapToolbarItem } from "@/features/map/core/components/toolbar";
|
|
||||||
import { MapZoom } from "@/features/map/core/components/zoom";
|
|
||||||
import {
|
|
||||||
MAP_CONTROL_SURFACE_CLASS_NAME,
|
|
||||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME
|
|
||||||
} from "@/features/map/core/components/map-control-styles";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { env } from "@/shared/config/env";
|
import { env } from "@/shared/config/env";
|
||||||
import { AgentTaskTicker } from "./components/agent-task-ticker";
|
import { AgentTaskTicker } from "./components/agent-task-ticker";
|
||||||
import { MapDevPanel } from "./components/map-dev-panel";
|
import { MapDevPanel } from "./components/map-dev-panel";
|
||||||
import { FeaturePopover } from "./components/feature-popover";
|
import { FeaturePopover } from "./components/feature-popover";
|
||||||
import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
|
import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
|
||||||
|
import { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
|
||||||
import {
|
import {
|
||||||
ToolbarPanel,
|
ToolbarPanel,
|
||||||
type ExportViewPreset,
|
type ExportViewPreset,
|
||||||
@@ -48,15 +39,12 @@ import {
|
|||||||
type ToolbarToolId
|
type ToolbarToolId
|
||||||
} from "./components/toolbar-panel";
|
} from "./components/toolbar-panel";
|
||||||
import { WorkbenchTopBar } from "./components/workbench-top-bar";
|
import { WorkbenchTopBar } from "./components/workbench-top-bar";
|
||||||
import {
|
|
||||||
SCHEDULED_CONDITION_REFRESH_INTERVAL_MS,
|
|
||||||
createOperationalScheduledConditions
|
|
||||||
} from "./data/scheduled-conditions";
|
|
||||||
import { WORKBENCH_SCENARIOS, WORKBENCH_USER } from "./data/workbench-session";
|
import { WORKBENCH_SCENARIOS, WORKBENCH_USER } from "./data/workbench-session";
|
||||||
import { useWorkbenchAgent } from "./hooks/use-workbench-agent";
|
import { useWorkbenchAgent } from "./hooks/use-workbench-agent";
|
||||||
import { useWorkbenchDrawing, type WorkbenchDrawMode } from "./hooks/use-workbench-drawing";
|
import { useWorkbenchDrawing, type WorkbenchDrawMode } from "./hooks/use-workbench-drawing";
|
||||||
import { useWorkbenchMap } from "./hooks/use-workbench-map";
|
import { useWorkbenchMap } from "./hooks/use-workbench-map";
|
||||||
import { useWorkbenchMapController } from "./hooks/use-workbench-map-controller";
|
import { useWorkbenchMapController } from "./hooks/use-workbench-map-controller";
|
||||||
|
import { useWorkbenchRuntimeData } from "./hooks/use-workbench-runtime-data";
|
||||||
import { toMapFeatureReference } from "./hooks/use-map-interactions";
|
import { toMapFeatureReference } from "./hooks/use-map-interactions";
|
||||||
import {
|
import {
|
||||||
useWorkbenchMeasurement,
|
useWorkbenchMeasurement,
|
||||||
@@ -65,20 +53,18 @@ import {
|
|||||||
} from "./hooks/use-workbench-measurement";
|
} from "./hooks/use-workbench-measurement";
|
||||||
import {
|
import {
|
||||||
WORKBENCH_LAYOUT,
|
WORKBENCH_LAYOUT,
|
||||||
clampAgentPanelWidth,
|
|
||||||
getWorkbenchBasemapTone,
|
getWorkbenchBasemapTone,
|
||||||
getWorkbenchLayoutCssVariables,
|
getWorkbenchLayoutCssVariables
|
||||||
getWorkbenchViewportLayout
|
|
||||||
} from "./layout/workbench-layout";
|
} from "./layout/workbench-layout";
|
||||||
import { getResponsiveWorkbenchPadding } from "./map/camera";
|
import { getResponsiveWorkbenchPadding } from "./map/camera";
|
||||||
import { exportMapViewImage } from "./map/export-view";
|
import { exportMapViewImage } from "./map/export-view";
|
||||||
import { parseScadaAnalysisItems } from "./map/scada-analysis";
|
import { parseScadaAnalysisItems } from "./map/scada-analysis";
|
||||||
import {
|
import {
|
||||||
BASE_LAYER_IDS,
|
|
||||||
BASE_LAYER_OPTIONS,
|
BASE_LAYER_OPTIONS,
|
||||||
INITIAL_LAYER_VISIBILITY,
|
INITIAL_LAYER_VISIBILITY,
|
||||||
MAP_LEGEND_ITEMS,
|
MAP_LEGEND_ITEMS,
|
||||||
WORKBENCH_LAYER_GROUPS,
|
WORKBENCH_LAYER_GROUPS,
|
||||||
|
applyBaseLayerVisibility,
|
||||||
createLayerControlItems,
|
createLayerControlItems,
|
||||||
getWorkbenchLayerIds
|
getWorkbenchLayerIds
|
||||||
} from "./map/map-control-config";
|
} from "./map/map-control-config";
|
||||||
@@ -89,49 +75,23 @@ import type {
|
|||||||
ScheduledConditionRecord,
|
ScheduledConditionRecord,
|
||||||
WorkbenchAlert
|
WorkbenchAlert
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import {
|
||||||
|
createScheduledConditionAlerts,
|
||||||
|
getScheduledConditionIdFromAlertId
|
||||||
|
} from "./utils/scheduled-condition-alerts";
|
||||||
import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-prompts";
|
import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-prompts";
|
||||||
|
|
||||||
const HEADER_DATA_TIME_STEP_MINUTES = 5;
|
|
||||||
const INITIAL_HEADER_DATA_TIME = "同步中";
|
|
||||||
const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables();
|
const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables();
|
||||||
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 MapWorkbenchPage() {
|
export function MapWorkbenchPage() {
|
||||||
const hasMapboxToken = Boolean(env.TJWATER_MAPBOX_ACCESS_TOKEN);
|
const hasMapboxToken = Boolean(env.TJWATER_MAPBOX_ACCESS_TOKEN);
|
||||||
const devPanelEnabled = env.TJWATER_ENABLE_DEV_PANEL;
|
const devPanelEnabled = env.TJWATER_ENABLE_DEV_PANEL;
|
||||||
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const desktopAgentPanelRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const agentPanelResizeLeftRef = useRef(0);
|
|
||||||
const agentPanelResizeCleanupRef = useRef<(() => void) | null>(null);
|
|
||||||
const [detailFeature, setDetailFeature] = useState<DetailFeature | null>(null);
|
const [detailFeature, setDetailFeature] = useState<DetailFeature | null>(null);
|
||||||
const [devPanelOpen, setDevPanelOpen] = useState(false);
|
const [devPanelOpen, setDevPanelOpen] = useState(false);
|
||||||
const [impactVisible, setImpactVisible] = useState(false);
|
const [impactVisible, setImpactVisible] = useState(false);
|
||||||
const [isLargeScreen, setIsLargeScreen] = useState(false);
|
const [isLargeScreen, setIsLargeScreen] = useState(false);
|
||||||
const [viewportWidth, setViewportWidth] = useState<number>(WORKBENCH_LAYOUT.desktopMinWidth);
|
const [viewportWidth, setViewportWidth] = useState<number>(WORKBENCH_LAYOUT.desktopMinWidth);
|
||||||
const [agentPanelWidth, setAgentPanelWidth] = useState<number | null>(null);
|
|
||||||
const [agentPanelResizing, setAgentPanelResizing] = useState(false);
|
|
||||||
const [activeToolId, setActiveToolId] = useState<ToolbarToolId | null>(null);
|
const [activeToolId, setActiveToolId] = useState<ToolbarToolId | null>(null);
|
||||||
const [conditionFeedVisible, setConditionFeedVisible] = useState(false);
|
const [conditionFeedVisible, setConditionFeedVisible] = useState(false);
|
||||||
const [conditionFeedMounted, setConditionFeedMounted] = useState(true);
|
const [conditionFeedMounted, setConditionFeedMounted] = useState(true);
|
||||||
@@ -153,9 +113,8 @@ export function MapWorkbenchPage() {
|
|||||||
const [layerVisibility, setLayerVisibility] =
|
const [layerVisibility, setLayerVisibility] =
|
||||||
useState<Record<string, boolean>>(INITIAL_LAYER_VISIBILITY);
|
useState<Record<string, boolean>>(INITIAL_LAYER_VISIBILITY);
|
||||||
const [agentUiResults, setAgentUiResults] = useState<AgentUiResult[]>([]);
|
const [agentUiResults, setAgentUiResults] = useState<AgentUiResult[]>([]);
|
||||||
const [scheduledConditions, setScheduledConditions] = useState<ScheduledConditionItem[]>([]);
|
const { headerDataTime, scheduledConditions, scheduledConditionsLoading } =
|
||||||
const [scheduledConditionsLoading, setScheduledConditionsLoading] = useState(true);
|
useWorkbenchRuntimeData();
|
||||||
const [headerDataTime, setHeaderDataTime] = useState(INITIAL_HEADER_DATA_TIME);
|
|
||||||
const baseLayerOptions = useMemo(
|
const baseLayerOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
BASE_LAYER_OPTIONS.map((option) => ({
|
BASE_LAYER_OPTIONS.map((option) => ({
|
||||||
@@ -174,9 +133,6 @@ export function MapWorkbenchPage() {
|
|||||||
const handleChange = () => {
|
const handleChange = () => {
|
||||||
setIsLargeScreen(mediaQuery.matches);
|
setIsLargeScreen(mediaQuery.matches);
|
||||||
setViewportWidth(window.innerWidth);
|
setViewportWidth(window.innerWidth);
|
||||||
setAgentPanelWidth((currentWidth) =>
|
|
||||||
currentWidth === null ? null : clampAgentPanelWidth(currentWidth, window.innerWidth)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChange();
|
handleChange();
|
||||||
@@ -190,38 +146,6 @@ export function MapWorkbenchPage() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => () => agentPanelResizeCleanupRef.current?.(), []);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const shouldShowConditionFeed = conditionFeedVisible && !activeToolId;
|
const shouldShowConditionFeed = conditionFeedVisible && !activeToolId;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -751,7 +675,7 @@ export function MapWorkbenchPage() {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = "drainage-network-map-config.json";
|
link.download = "supply-network-map-config.json";
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
@@ -829,69 +753,6 @@ export function MapWorkbenchPage() {
|
|||||||
onRejectQuestion: agent.rejectQuestion
|
onRejectQuestion: agent.rejectQuestion
|
||||||
};
|
};
|
||||||
|
|
||||||
const resizeAgentPanel = useCallback((clientX: number) => {
|
|
||||||
setAgentPanelWidth(
|
|
||||||
clampAgentPanelWidth(clientX - agentPanelResizeLeftRef.current, window.innerWidth)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAgentPanelResizeStart = useCallback(
|
|
||||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
||||||
if (event.button !== 0 || !desktopAgentPanelRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
agentPanelResizeLeftRef.current = desktopAgentPanelRef.current.getBoundingClientRect().left;
|
|
||||||
setAgentPanelResizing(true);
|
|
||||||
resizeAgentPanel(event.clientX);
|
|
||||||
|
|
||||||
agentPanelResizeCleanupRef.current?.();
|
|
||||||
const handlePointerMove = (pointerEvent: PointerEvent) => {
|
|
||||||
pointerEvent.preventDefault();
|
|
||||||
resizeAgentPanel(pointerEvent.clientX);
|
|
||||||
};
|
|
||||||
const stopResizing = () => {
|
|
||||||
agentPanelResizeCleanupRef.current?.();
|
|
||||||
setAgentPanelResizing(false);
|
|
||||||
};
|
|
||||||
const cleanup = () => {
|
|
||||||
window.removeEventListener("pointermove", handlePointerMove);
|
|
||||||
window.removeEventListener("pointerup", stopResizing);
|
|
||||||
window.removeEventListener("pointercancel", stopResizing);
|
|
||||||
agentPanelResizeCleanupRef.current = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
agentPanelResizeCleanupRef.current = cleanup;
|
|
||||||
window.addEventListener("pointermove", handlePointerMove);
|
|
||||||
window.addEventListener("pointerup", stopResizing);
|
|
||||||
window.addEventListener("pointercancel", stopResizing);
|
|
||||||
},
|
|
||||||
[resizeAgentPanel]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAgentPanelResizeKeyDown = useCallback((event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
||||||
const currentWidth =
|
|
||||||
desktopAgentPanelRef.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();
|
|
||||||
setAgentPanelWidth(clampAgentPanelWidth(nextWidth, window.innerWidth));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className="relative h-[100dvh] min-h-[640px] overflow-hidden bg-slate-100 text-slate-900 tabular-nums"
|
className="relative h-[100dvh] min-h-[640px] overflow-hidden bg-slate-100 text-slate-900 tabular-nums"
|
||||||
@@ -935,47 +796,20 @@ export function MapWorkbenchPage() {
|
|||||||
onExportConfig={handleExportConfig}
|
onExportConfig={handleExportConfig}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<WorkbenchAgentPanels
|
||||||
ref={desktopAgentPanelRef}
|
panelProps={agentPanelProps}
|
||||||
className="pointer-events-none absolute bottom-4 left-3 top-24 z-20 hidden w-[var(--workbench-agent-width)] max-w-[50vw] 2xl:w-[var(--workbench-agent-width-wide)] lg:block"
|
panelOpen={agent.panelOpen}
|
||||||
style={agentPanelWidth === null ? undefined : { width: agentPanelWidth }}
|
panelCollapsing={agent.panelCollapsing}
|
||||||
>
|
mobileOpen={agent.mobileOpen}
|
||||||
{agent.panelOpen ? (
|
mobilePanelCollapsing={agent.mobilePanelCollapsing}
|
||||||
<>
|
|
||||||
<AgentCommandPanel
|
|
||||||
{...agentPanelProps}
|
|
||||||
collapsing={agent.panelCollapsing}
|
|
||||||
onCollapse={agent.collapsePanel}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
aria-label="调整 Agent 面板宽度"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-valuemax={Math.round(viewportWidth * WORKBENCH_LAYOUT.maxAgentViewportRatio)}
|
|
||||||
aria-valuemin={getWorkbenchViewportLayout(viewportWidth).agentWidth}
|
|
||||||
aria-valuenow={Math.round(
|
|
||||||
agentPanelWidth ?? getWorkbenchViewportLayout(viewportWidth).agentWidth
|
|
||||||
)}
|
|
||||||
className={cn(
|
|
||||||
"group pointer-events-auto absolute -right-3 top-4 bottom-4 z-10 flex w-6 cursor-ew-resize touch-none items-center justify-center outline-hidden",
|
|
||||||
agentPanelResizing && "[&>span]:bg-blue-500 [&>span]:opacity-100"
|
|
||||||
)}
|
|
||||||
role="separator"
|
|
||||||
tabIndex={0}
|
|
||||||
title="拖拽调整 Agent 面板宽度"
|
|
||||||
onKeyDown={handleAgentPanelResizeKeyDown}
|
|
||||||
onPointerDown={handleAgentPanelResizeStart}
|
|
||||||
>
|
|
||||||
<span className="h-14 w-1 rounded-full bg-slate-500/60 opacity-50 shadow-xs transition-[height,background-color,opacity] group-hover:h-20 group-hover:bg-blue-500 group-hover:opacity-100 group-focus-visible:h-20 group-focus-visible:bg-blue-500 group-focus-visible:opacity-100" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<AgentCollapsedRail
|
|
||||||
personaState={agent.personaState}
|
personaState={agent.personaState}
|
||||||
statusLabel={agent.statusLabel}
|
statusLabel={agent.statusLabel}
|
||||||
onExpand={agent.expandPanel}
|
viewportWidth={viewportWidth}
|
||||||
|
onCollapsePanel={agent.collapsePanel}
|
||||||
|
onCloseMobilePanel={agent.closeMobilePanel}
|
||||||
|
onExpandPanel={agent.expandPanel}
|
||||||
|
onOpenMobilePanel={agent.openMobilePanel}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!devPanelOpen ? (
|
{!devPanelOpen ? (
|
||||||
<div className="absolute right-2 top-24 z-20">
|
<div className="absolute right-2 top-24 z-20">
|
||||||
@@ -1017,22 +851,6 @@ export function MapWorkbenchPage() {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="absolute bottom-20 left-3 right-3 z-30 lg:hidden">
|
|
||||||
{agent.mobileOpen ? (
|
|
||||||
<div className="h-[calc(100dvh-8.5rem)] min-h-[420px]">
|
|
||||||
<AgentCommandPanel
|
|
||||||
{...agentPanelProps}
|
|
||||||
collapsing={agent.mobilePanelCollapsing}
|
|
||||||
onCollapse={agent.closeMobilePanel}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!agent.mobileOpen ? (
|
|
||||||
<MobileAgentToggle personaState={agent.personaState} onOpen={agent.openMobilePanel} />
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!agent.mobileOpen ? (
|
{!agent.mobileOpen ? (
|
||||||
<div className="absolute bottom-20 left-3 right-3 z-30 lg:hidden">
|
<div className="absolute bottom-20 left-3 right-3 z-30 lg:hidden">
|
||||||
<ToolbarPanel
|
<ToolbarPanel
|
||||||
@@ -1118,18 +936,6 @@ function isNoticeSourceStatus(sourceStatus: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBaseLayerVisibility(map: MapLibreMap, activeBaseLayerId: string) {
|
|
||||||
BASE_LAYER_IDS.forEach((layerId) => {
|
|
||||||
if (map.getLayer(layerId)) {
|
|
||||||
map.setLayoutProperty(
|
|
||||||
layerId,
|
|
||||||
"visibility",
|
|
||||||
activeBaseLayerId === layerId ? "visible" : "none"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExportTargetLongEdge(preset: ExportViewPreset) {
|
function getExportTargetLongEdge(preset: ExportViewPreset) {
|
||||||
if (preset === "4k") {
|
if (preset === "4k") {
|
||||||
return 3840;
|
return 3840;
|
||||||
@@ -1145,67 +951,3 @@ function getExportPresetLabel(preset: ExportViewPreset) {
|
|||||||
|
|
||||||
return "当前分辨率";
|
return "当前分辨率";
|
||||||
}
|
}
|
||||||
|
|
||||||
function createScheduledConditionAlerts(conditions: ScheduledConditionItem[]): WorkbenchAlert[] {
|
|
||||||
return conditions
|
|
||||||
.filter(isAlertCondition)
|
|
||||||
.sort((a, b) => getScheduledConditionTimestamp(b) - getScheduledConditionTimestamp(a))
|
|
||||||
.map((condition) => {
|
|
||||||
return {
|
|
||||||
id: `scheduled-condition-alert-${condition.id}`,
|
|
||||||
title: `待复核:${condition.title}`,
|
|
||||||
description: condition.detail ?? condition.summary,
|
|
||||||
time: formatScheduledConditionTime(condition.scheduledAt)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAlertCondition(condition: ScheduledConditionItem) {
|
|
||||||
return condition.kind === "condition" && condition.status === "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getScheduledConditionTimestamp(condition: ScheduledConditionItem) {
|
|
||||||
const scheduledAt = Date.parse(condition.scheduledAt);
|
|
||||||
return Number.isNaN(scheduledAt) ? condition.updatedAt : scheduledAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatScheduledConditionTime(value: string) {
|
|
||||||
const match = value.match(/T(\d{2}:\d{2})/);
|
|
||||||
return match?.[1] ?? value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SCHEDULED_CONDITION_ALERT_ID_PREFIX = "scheduled-condition-alert-";
|
|
||||||
|
|
||||||
function getScheduledConditionIdFromAlertId(alertId: string) {
|
|
||||||
if (!alertId.startsWith(SCHEDULED_CONDITION_ALERT_ID_PREFIX)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return alertId.slice(SCHEDULED_CONDITION_ALERT_ID_PREFIX.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MobileAgentToggle({
|
|
||||||
personaState,
|
|
||||||
onOpen
|
|
||||||
}: {
|
|
||||||
personaState: ComponentProps<typeof AgentPersona>["state"];
|
|
||||||
onOpen: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="absolute bottom-20 left-3 z-30 lg:hidden">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="打开 Agent 面板"
|
|
||||||
title="打开 Agent 面板"
|
|
||||||
onClick={onOpen}
|
|
||||||
className={cn(
|
|
||||||
"grid h-12 w-12 place-items-center text-violet-700",
|
|
||||||
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
|
|
||||||
MAP_CONTROL_SURFACE_CLASS_NAME
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AgentPersona className="pointer-events-none h-10 w-10" state={personaState} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -119,5 +119,5 @@ function getExportLabel(targetLongEdge: number | undefined, scale: number) {
|
|||||||
|
|
||||||
function getDefaultExportFilename(label: string) {
|
function getDefaultExportFilename(label: string) {
|
||||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
|
||||||
return `drainage-network-view-${label}-${timestamp}.png`;
|
return `supply-network-view-${label}-${timestamp}.png`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ import { env } from "@/shared/config/env";
|
|||||||
|
|
||||||
export const MAP_URL = env.TJWATER_MAP_URL.replace(/\/$/, "");
|
export const MAP_URL = env.TJWATER_MAP_URL.replace(/\/$/, "");
|
||||||
|
|
||||||
export const GEOSERVER_WORKSPACE = env.TJWATER_GEOSERVER_WORKSPACE;
|
export const GEOSERVER_WORKSPACE = env.TJWATER_GEOSERVER_WORKSPACE.trim().replace(/^\/+|\/+$/g, "");
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||||
import type { BaseLayerOption } from "@/features/map/core/components/base-layers-control";
|
import type { BaseLayerOption, MapLayerControlItem, MapLegendItem } from "@/features/map/core";
|
||||||
import type { MapLayerControlItem } from "@/features/map/core/components/layer-control";
|
import { GEOSERVER_WORKSPACE } from "./geoserver-config";
|
||||||
import type { MapLegendItem } from "@/features/map/core/components/legend";
|
|
||||||
import { GEOSERVER_WORKSPACE } from "../../../lib/config";
|
|
||||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||||
import {
|
import {
|
||||||
AVAILABLE_WATER_NETWORK_SOURCE_IDS,
|
AVAILABLE_WATER_NETWORK_SOURCE_IDS,
|
||||||
@@ -57,6 +55,18 @@ export const MAP_LEGEND_ITEMS: MapLegendItem[] = [
|
|||||||
|
|
||||||
export const BASE_LAYER_IDS = ["mapbox-light", "mapbox-satellite"] as const;
|
export const BASE_LAYER_IDS = ["mapbox-light", "mapbox-satellite"] as const;
|
||||||
|
|
||||||
|
export function applyBaseLayerVisibility(map: MapLibreMap, activeBaseLayerId: string) {
|
||||||
|
BASE_LAYER_IDS.forEach((layerId) => {
|
||||||
|
if (map.getLayer(layerId)) {
|
||||||
|
map.setLayoutProperty(
|
||||||
|
layerId,
|
||||||
|
"visibility",
|
||||||
|
activeBaseLayerId === layerId ? "visible" : "none"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const BASE_LAYER_OPTIONS: BaseLayerOption[] = [
|
export const BASE_LAYER_OPTIONS: BaseLayerOption[] = [
|
||||||
{
|
{
|
||||||
id: "mapbox-light",
|
id: "mapbox-light",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { FeatureCollection } from "geojson";
|
import type { FeatureCollection } from "geojson";
|
||||||
import { GEOSERVER_WORKSPACE, MAP_URL } from "../../../lib/config";
|
import { GEOSERVER_WORKSPACE, MAP_URL } from "./geoserver-config";
|
||||||
import {
|
import {
|
||||||
SOURCE_LAYERS,
|
SOURCE_LAYERS,
|
||||||
isAvailableWaterNetworkSourceId,
|
isAvailableWaterNetworkSourceId,
|
||||||
|
|||||||
@@ -139,70 +139,6 @@ const facilityIconSize = [
|
|||||||
24, 0.56
|
24, 0.56
|
||||||
] as IconSize;
|
] as IconSize;
|
||||||
|
|
||||||
export const DRAINAGE_LAYER_VISUALS = {
|
|
||||||
conduits: {
|
|
||||||
minzoom: 9,
|
|
||||||
casingWidth: [
|
|
||||||
"interpolate", ["linear"], ["zoom"],
|
|
||||||
9, 1.7,
|
|
||||||
13, 2.8,
|
|
||||||
17, ["+", 4.5, ["/", ["coalesce", ["get", "diameter"], 100], 300]],
|
|
||||||
22, ["+", 6, ["/", ["coalesce", ["get", "diameter"], 100], 260]],
|
|
||||||
24, ["+", 7, ["/", ["coalesce", ["get", "diameter"], 100], 240]]
|
|
||||||
] as LineWidth,
|
|
||||||
width: [
|
|
||||||
"interpolate", ["linear"], ["zoom"],
|
|
||||||
9, 1.1,
|
|
||||||
13, 2,
|
|
||||||
17, ["+", 2.5, ["/", ["coalesce", ["get", "diameter"], 100], 360]],
|
|
||||||
22, ["+", 3.5, ["/", ["coalesce", ["get", "diameter"], 100], 300]],
|
|
||||||
24, ["+", 4, ["/", ["coalesce", ["get", "diameter"], 100], 280]]
|
|
||||||
] as LineWidth,
|
|
||||||
hoverOutlineWidth: ["interpolate", ["linear"], ["zoom"], 9, 5.5, 13, 8, 17, 12.5, 22, 15, 24, 17] as LineWidth,
|
|
||||||
hoverWidth: ["interpolate", ["linear"], ["zoom"], 9, 3, 13, 5.2, 17, 9, 22, 11.5, 24, 13] as LineWidth,
|
|
||||||
selectedWidth: ["interpolate", ["linear"], ["zoom"], 9, 4, 13, 6.5, 17, 10.5, 22, 13.5, 24, 16] as LineWidth,
|
|
||||||
selectedBorderWidth: ["interpolate", ["linear"], ["zoom"], 9, 1.2, 13, 1.5, 17, 2, 22, 2.5, 24, 3] as LineWidth,
|
|
||||||
hitWidth: lineHitWidth
|
|
||||||
},
|
|
||||||
junctions: {
|
|
||||||
minzoom: 12,
|
|
||||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 12, 1.2, 14, 1.8, 15, 2.8, 16, 4.5, 20, 7, 24, 9.5] as CircleRadius,
|
|
||||||
haloOpacity: ["interpolate", ["linear"], ["zoom"], 12, 0, 14, 0, 15, 1] as const,
|
|
||||||
radius: ["interpolate", ["linear"], ["zoom"], 12, 0.6, 14, 1, 15, 1.5, 16, 3, 20, 5.5, 24, 8] as CircleRadius,
|
|
||||||
hoverRadius: ["interpolate", ["linear"], ["zoom"], 12, 5, 16, 7, 20, 10, 24, 12] as CircleRadius,
|
|
||||||
selectedRadius: ["interpolate", ["linear"], ["zoom"], 12, 5.5, 16, 7.5, 20, 10.5, 24, 13] as CircleRadius,
|
|
||||||
hitRadius: pointHitRadius
|
|
||||||
},
|
|
||||||
orifices: {
|
|
||||||
minzoom: 11,
|
|
||||||
casingWidth: ["interpolate", ["linear"], ["zoom"], 11, 3.2, 17, 7.5, 22, 9, 24, 10] as LineWidth,
|
|
||||||
width: ["interpolate", ["linear"], ["zoom"], 11, 1.6, 17, 4.5, 22, 6, 24, 7] as LineWidth,
|
|
||||||
hoverOutlineWidth: ["interpolate", ["linear"], ["zoom"], 11, 6, 17, 10.5, 22, 12.5, 24, 14] as LineWidth,
|
|
||||||
hoverWidth: ["interpolate", ["linear"], ["zoom"], 11, 4, 17, 7, 22, 9, 24, 10.5] as LineWidth,
|
|
||||||
selectedWidth: ["interpolate", ["linear"], ["zoom"], 11, 5.5, 17, 9, 22, 12, 24, 14] as LineWidth,
|
|
||||||
selectedBorderWidth: ["interpolate", ["linear"], ["zoom"], 11, 1.25, 17, 1.75, 22, 2.25, 24, 2.5] as LineWidth,
|
|
||||||
hitWidth: lineHitWidth
|
|
||||||
},
|
|
||||||
pumps: {
|
|
||||||
minzoom: 11,
|
|
||||||
casingWidth: ["interpolate", ["linear"], ["zoom"], 11, 4, 17, 9.5, 22, 11, 24, 12] as LineWidth,
|
|
||||||
width: ["interpolate", ["linear"], ["zoom"], 11, 2.4, 17, 6.2, 22, 8, 24, 9] as LineWidth,
|
|
||||||
hoverOutlineWidth: ["interpolate", ["linear"], ["zoom"], 11, 7, 17, 12, 22, 14, 24, 16] as LineWidth,
|
|
||||||
hoverWidth: ["interpolate", ["linear"], ["zoom"], 11, 4.5, 17, 8.5, 22, 10.5, 24, 12] as LineWidth,
|
|
||||||
selectedWidth: ["interpolate", ["linear"], ["zoom"], 11, 6, 17, 10, 22, 13, 24, 15] as LineWidth,
|
|
||||||
selectedBorderWidth: ["interpolate", ["linear"], ["zoom"], 11, 1.25, 17, 1.75, 22, 2.25, 24, 2.5] as LineWidth,
|
|
||||||
hitWidth: lineHitWidth
|
|
||||||
},
|
|
||||||
outfalls: {
|
|
||||||
minzoom: 11,
|
|
||||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 11, 5, 16, 7, 20, 9, 24, 11] as CircleRadius,
|
|
||||||
radius: ["interpolate", ["linear"], ["zoom"], 11, 3, 16, 5, 20, 7, 24, 9] as CircleRadius,
|
|
||||||
hoverRadius: ["interpolate", ["linear"], ["zoom"], 11, 6, 16, 8, 20, 11, 24, 13] as CircleRadius,
|
|
||||||
selectedRadius: ["interpolate", ["linear"], ["zoom"], 11, 7.5, 16, 9.5, 20, 12.5, 24, 15] as CircleRadius,
|
|
||||||
hitRadius: pointHitRadius
|
|
||||||
}
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const SUPPLY_LAYER_VISUALS = {
|
export const SUPPLY_LAYER_VISUALS = {
|
||||||
pipes: {
|
pipes: {
|
||||||
minzoom: 8.5,
|
minzoom: 8.5,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { StyleSpecification, VectorSourceSpecification } from "maplibre-gl";
|
import type { StyleSpecification, VectorSourceSpecification } from "maplibre-gl";
|
||||||
|
|
||||||
import { GEOSERVER_WORKSPACE, MAP_URL } from "../../../lib/config";
|
import { GEOSERVER_WORKSPACE, MAP_URL } from "./geoserver-config";
|
||||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||||
|
|
||||||
export const WATER_NETWORK_GLOBAL_VIEW = {
|
export const WATER_NETWORK_GLOBAL_VIEW = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BarChart3, Layers3, MapPinned, MoreHorizontal, Ruler } from "lucide-react";
|
import { BarChart3, Layers3, MapPinned, MoreHorizontal, Ruler } from "lucide-react";
|
||||||
import type { MapToolbarItem } from "@/features/map/core/components/toolbar";
|
import type { MapToolbarItem } from "@/features/map/core";
|
||||||
|
|
||||||
export const waterNetworkToolbarItems: MapToolbarItem[] = [
|
export const waterNetworkToolbarItems: MapToolbarItem[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { ScheduledConditionItem, WorkbenchAlert } from "../types";
|
||||||
|
|
||||||
|
const SCHEDULED_CONDITION_ALERT_ID_PREFIX = "scheduled-condition-alert-";
|
||||||
|
|
||||||
|
export function createScheduledConditionAlerts(
|
||||||
|
conditions: ScheduledConditionItem[]
|
||||||
|
): WorkbenchAlert[] {
|
||||||
|
return conditions
|
||||||
|
.filter(isAlertCondition)
|
||||||
|
.sort((a, b) => getScheduledConditionTimestamp(b) - getScheduledConditionTimestamp(a))
|
||||||
|
.map((condition) => ({
|
||||||
|
id: `${SCHEDULED_CONDITION_ALERT_ID_PREFIX}${condition.id}`,
|
||||||
|
title: `待复核:${condition.title}`,
|
||||||
|
description: condition.detail ?? condition.summary,
|
||||||
|
time: formatScheduledConditionTime(condition.scheduledAt)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScheduledConditionIdFromAlertId(alertId: string) {
|
||||||
|
if (!alertId.startsWith(SCHEDULED_CONDITION_ALERT_ID_PREFIX)) return null;
|
||||||
|
return alertId.slice(SCHEDULED_CONDITION_ALERT_ID_PREFIX.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAlertCondition(condition: ScheduledConditionItem) {
|
||||||
|
return condition.kind === "condition" && condition.status === "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScheduledConditionTimestamp(condition: ScheduledConditionItem) {
|
||||||
|
const scheduledAt = Date.parse(condition.scheduledAt);
|
||||||
|
return Number.isNaN(scheduledAt) ? condition.updatedAt : scheduledAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatScheduledConditionTime(value: string) {
|
||||||
|
const match = value.match(/T(\d{2}:\d{2})/);
|
||||||
|
return match?.[1] ?? value;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getRunningWorkflowDefinition } from "@/features/workbench/data/running-workflows";
|
import { getRunningWorkflowDefinition } from "../data/running-workflows";
|
||||||
import type { ScheduledConditionItem, ScheduledConditionStatus, WorkbenchAlert } from "@/features/workbench/types";
|
import type { ScheduledConditionItem, ScheduledConditionStatus, WorkbenchAlert } from "../types";
|
||||||
|
|
||||||
const SCHEDULED_CONDITION_ALERT_ID_PREFIX = "scheduled-condition-alert-";
|
const SCHEDULED_CONDITION_ALERT_ID_PREFIX = "scheduled-condition-alert-";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { clsx, type ClassValue } from "clsx";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs));
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
@@ -7,7 +5,7 @@ import {
|
|||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/shared/ui/accordion";
|
} from "@/shared/ui/accordion";
|
||||||
import { Badge } from "@/shared/ui/badge";
|
import { Badge } from "@/shared/ui/badge";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type { Tool } from "ai";
|
import type { Tool } from "ai";
|
||||||
import { BotIcon } from "lucide-react";
|
import { BotIcon } from "lucide-react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -8,7 +6,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/shared/ui/select";
|
} from "@/shared/ui/select";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||||
import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
|
import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type { UIMessage } from "ai";
|
import type { UIMessage } from "ai";
|
||||||
import { ArrowDownIcon, DownloadIcon } from "lucide-react";
|
import { ArrowDownIcon, DownloadIcon } from "lucide-react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import {
|
import {
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
@@ -11,7 +9,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/shared/ui/tooltip";
|
} from "@/shared/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { cjk } from "@streamdown/cjk";
|
import { cjk } from "@streamdown/cjk";
|
||||||
import { code } from "@streamdown/code";
|
import { code } from "@streamdown/code";
|
||||||
import { createMathPlugin } from "@streamdown/math";
|
import { createMathPlugin } from "@streamdown/math";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { RiveParameters } from "@rive-app/react-webgl2";
|
import type { RiveParameters } from "@rive-app/react-webgl2";
|
||||||
import { useRive, useStateMachineInput } from "@rive-app/react-webgl2";
|
import { useRive, useStateMachineInput } from "@rive-app/react-webgl2";
|
||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
|
|
||||||
const RIVE_SOURCE = "https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/obsidian-2.0.riv";
|
const RIVE_SOURCE = "https://ejiidnob33g9ap1r.public.blob.vercel-storage.com/obsidian-2.0.riv";
|
||||||
const STATE_MACHINE = "default";
|
const STATE_MACHINE = "default";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -15,7 +13,7 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/shared/ui/collapsible";
|
} from "@/shared/ui/collapsible";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { ChevronsUpDownIcon } from "lucide-react";
|
import { ChevronsUpDownIcon } from "lucide-react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { createContext, useContext, useMemo } from "react";
|
import { createContext, useContext, useMemo } from "react";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -39,7 +37,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/shared/ui/tooltip";
|
} from "@/shared/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";
|
import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";
|
||||||
import {
|
import {
|
||||||
CornerDownLeftIcon,
|
CornerDownLeftIcon,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
"use client";
|
import { cn } from "@/shared/ui/cn";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { MotionProps } from "motion/react";
|
import type { MotionProps } from "motion/react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import type { CSSProperties, ElementType, JSX } from "react";
|
import type { CSSProperties, ElementType, JSX } from "react";
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/shared/ui/cn";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
|
|
||||||
export type SuggestionsProps = ComponentProps<"div">;
|
export type SuggestionsProps = ComponentProps<"div">;
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
import { ChevronDown } from "lucide-react"
|
import { ChevronDown } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const Accordion = AccordionPrimitive.Root
|
const Accordion = AccordionPrimitive.Root
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
import { Separator } from "@/shared/ui/separator"
|
import { Separator } from "@/shared/ui/separator"
|
||||||
|
|
||||||
const buttonGroupVariants = cva(
|
const buttonGroupVariants = cva(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as React from "react"
|
|||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
const Card = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||||
|
|
||||||
const Collapsible = CollapsiblePrimitive.Root
|
const Collapsible = CollapsiblePrimitive.Root
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
import { Search } from "lucide-react"
|
import { Search } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
import { Dialog, DialogContent } from "@/shared/ui/dialog"
|
import { Dialog, DialogContent } from "@/shared/ui/dialog"
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
const Command = React.forwardRef<
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const HoverCard = HoverCardPrimitive.Root
|
const HoverCard = HoverCardPrimitive.Root
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
import { Button } from "@/shared/ui/button"
|
import { Button } from "@/shared/ui/button"
|
||||||
import { Input } from "@/shared/ui/input"
|
import { Input } from "@/shared/ui/input"
|
||||||
import { Textarea } from "@/shared/ui/textarea"
|
import { Textarea } from "@/shared/ui/textarea"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
const Separator = React.forwardRef<
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Loader2Icon } from "lucide-react"
|
import { Loader2Icon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const Textarea = React.forwardRef<
|
const Textarea = React.forwardRef<
|
||||||
HTMLTextAreaElement,
|
HTMLTextAreaElement,
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/shared/ui/cn"
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||||
|
import { join, relative } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const sourceRoot = join(process.cwd(), "src");
|
||||||
|
|
||||||
|
function listRuntimeFiles(directory: string): string[] {
|
||||||
|
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const target = join(directory, entry.name);
|
||||||
|
if (entry.isDirectory()) return listRuntimeFiles(target);
|
||||||
|
if (!/\.(ts|tsx)$/.test(entry.name) || /\.(test|e2e)\.(ts|tsx)$/.test(entry.name)) return [];
|
||||||
|
return [target];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMatches(directory: string, pattern: RegExp): string[] {
|
||||||
|
return listRuntimeFiles(directory).flatMap((file) => {
|
||||||
|
const source = readFileSync(file, "utf8");
|
||||||
|
return pattern.test(source) ? [relative(sourceRoot, file)] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("frontend module boundaries", () => {
|
||||||
|
it("uses public feature entry points for absolute feature imports", () => {
|
||||||
|
expect(findMatches(sourceRoot, /@\/features\/(?:agent|workbench|map\/core)\//)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps shared independent from app, features, and mocks", () => {
|
||||||
|
expect(findMatches(join(sourceRoot, "shared"), /@\/(?:app|features|mocks)\//)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps map core independent from Agent and workbench code", () => {
|
||||||
|
expect(
|
||||||
|
findMatches(
|
||||||
|
join(sourceRoot, "features/map/core"),
|
||||||
|
/@\/features\/(?:agent|workbench)(?:\/|["'])/
|
||||||
|
)
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps Agent code independent from workbench code", () => {
|
||||||
|
expect(
|
||||||
|
findMatches(join(sourceRoot, "features/agent"), /@\/features\/workbench(?:\/|["'])/)
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not restore Next.js client directives or the catch-all lib directory", () => {
|
||||||
|
expect(findMatches(sourceRoot, /^["']use client["'];?/m)).toEqual([]);
|
||||||
|
expect(existsSync(join(sourceRoot, "lib"))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,14 +2,17 @@ import { readdirSync, readFileSync } from "node:fs";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const featuresRoot = join(process.cwd(), "src/features");
|
const runtimeRoot = join(process.cwd(), "src");
|
||||||
const forbiddenDrainageTerms = [
|
const forbiddenDrainageTerms = [
|
||||||
"排水管网",
|
"排水管网",
|
||||||
"单位排水电耗",
|
"单位排水电耗",
|
||||||
"排水边界",
|
"排水边界",
|
||||||
"距污水厂距离",
|
"距污水厂距离",
|
||||||
"关联检查井",
|
"关联检查井",
|
||||||
"管渠拓扑"
|
"管渠拓扑",
|
||||||
|
"drainage-network",
|
||||||
|
"DRAINAGE_",
|
||||||
|
"DrainageFeature"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function listRuntimeFiles(directory: string): string[] {
|
function listRuntimeFiles(directory: string): string[] {
|
||||||
@@ -23,11 +26,11 @@ function listRuntimeFiles(directory: string): string[] {
|
|||||||
|
|
||||||
describe("supply domain boundary", () => {
|
describe("supply domain boundary", () => {
|
||||||
it("does not reintroduce drainage-only operating language", () => {
|
it("does not reintroduce drainage-only operating language", () => {
|
||||||
const violations = listRuntimeFiles(featuresRoot).flatMap((file) => {
|
const violations = listRuntimeFiles(runtimeRoot).flatMap((file) => {
|
||||||
const source = readFileSync(file, "utf8");
|
const source = readFileSync(file, "utf8");
|
||||||
return forbiddenDrainageTerms
|
return forbiddenDrainageTerms
|
||||||
.filter((term) => source.includes(term))
|
.filter((term) => source.includes(term))
|
||||||
.map((term) => `${file.replace(`${featuresRoot}/`, "")}: ${term}`);
|
.map((term) => `${file.replace(`${runtimeRoot}/`, "")}: ${term}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(violations).toEqual([]);
|
expect(violations).toEqual([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user