Compare commits
8 Commits
e5f13c3d46
...
latest
| Author | SHA1 | Date | |
|---|---|---|---|
| d80a071987 | |||
| 216c7b1ab9 | |||
| 7d966a5e91 | |||
| 22afdbf2e8 | |||
| ed9828befe | |||
| 968d798a2a | |||
| 7da0ed0e39 | |||
| 166b45e529 |
@@ -1,60 +0,0 @@
|
|||||||
# Copilot Instructions for TJWaterFrontend_Refine
|
|
||||||
|
|
||||||
## Environment Setup
|
|
||||||
|
|
||||||
1. **Node.js**: Ensure you have Node.js v18 or later installed.
|
|
||||||
2. **Dependencies**: Run `npm install` to install all project dependencies.
|
|
||||||
3. **Environment Variables**: Create a `.env.local` file in the root directory with
|
|
||||||
|
|
||||||
Using bash setup dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build, Test, and Lint
|
|
||||||
|
|
||||||
- **Dev Server**: `npm run dev` (Runs with increased memory limit: `--max_old_space_size=4096`)
|
|
||||||
- **Build**: `npm run build`
|
|
||||||
- **Lint**: `npm run lint` (ESLint)
|
|
||||||
- **Test**: `npm run test` (Jest)
|
|
||||||
- Run a specific test file: `npm run test -- <path/to/file>`
|
|
||||||
- Run a specific test case: `npm run test -- -t 'test name'`
|
|
||||||
|
|
||||||
## High-Level Architecture
|
|
||||||
|
|
||||||
- **Framework**: **Next.js 16 (App Router)** integrated with **Refine** (`@refinedev/core`).
|
|
||||||
- **Routing**:
|
|
||||||
- Routes are defined in `src/app`.
|
|
||||||
- Refine resources (e.g., `/network-simulation`, `/hydraulic-simulation/*`) map directly to these routes.
|
|
||||||
- Configuration is central in `src/app/_refine_context.tsx`.
|
|
||||||
- **State Management**:
|
|
||||||
- **Global App State**: **Zustand** (`src/store`).
|
|
||||||
- **Server State**: Managed by Refine hooks (`useList`, `useOne`, etc.) via **React Query**.
|
|
||||||
- **Authentication**:
|
|
||||||
- **NextAuth.js** handling Keycloak integration.
|
|
||||||
- Session token is synced to Zustand (`useAuthStore`) in `RefineContext`.
|
|
||||||
- **Data Layer**:
|
|
||||||
- Custom Data Provider: `src/providers/data-provider`.
|
|
||||||
- API Utilities: `src/lib/api.ts`, `src/lib/apiFetch.ts`.
|
|
||||||
- **UI & Styling**:
|
|
||||||
- **Material UI (MUI)**: Primary component library (`@mui/material`, `@refinedev/mui`).
|
|
||||||
- **Tailwind CSS v4**: Utility classes for layout and custom styling (`@tailwindcss/postcss`).
|
|
||||||
- **Mapping**: OpenLayers (`ol`), deck.gl, Turf.js.
|
|
||||||
- **Charts**: ECharts, MUI X Charts.
|
|
||||||
|
|
||||||
## Key Conventions
|
|
||||||
|
|
||||||
- **Refine Integration**:
|
|
||||||
- Use Refine hooks (`useTable`, `useForm`, `useNavigation`) for data-heavy components.
|
|
||||||
- Resources are defined in the `<Refine>` component in `src/app/_refine_context.tsx`.
|
|
||||||
- **Project Structure**:
|
|
||||||
- `src/components/`: Grouped by feature (e.g., `olmap`, `project`) or common UI elements.
|
|
||||||
- `src/lib/`: Utility functions and API helpers.
|
|
||||||
- `src/providers/`: Refine providers (data, etc.).
|
|
||||||
- **Imports**:
|
|
||||||
- Use absolute imports with `@/` alias (e.g., `@/components`, `@/store`, `@/lib`).
|
|
||||||
- _Note_: `@libs` alias in tsconfig points to non-existent `src/libs` folder; prefer `@/lib`.
|
|
||||||
- **Styling**:
|
|
||||||
- Prefer MUI components for standard UI elements.
|
|
||||||
- Use Tailwind utility classes for layout and custom overrides.
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
|
This repository is the TJWater web frontend built with Refine, Next.js, React, and MUI. Application source lives under the existing Next.js project folders. Reuse established page, component, provider, map, and chat patterns instead of adding parallel structures. Static assets and public files should remain in the existing asset/public locations. Build output (`.next/`), dependency folders, and local caches are generated and must not be edited by hand.
|
||||||
|
|
||||||
|
Deployment files are `Dockerfile`, `docker-compose.yml`, and `.gitea/workflows/package.yml`.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
|
Use npm and Node 20 or newer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
npm run lint
|
||||||
|
npm test
|
||||||
|
npm run test:coverage
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run dev` starts the Refine/Next development server. `npm run lint` runs ESLint. `npm test` runs Jest. `npm run build` creates the production build.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
|
||||||
|
Use TypeScript and React function components. Follow ESLint and Next.js conventions. Use `PascalCase` for components, `camelCase` for variables/functions, and descriptive feature-oriented filenames. Prefer MUI components and existing design tokens/patterns for UI. Keep operational screens dense, clear, and task-focused.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
Tests use Jest with React Testing Library. Name tests `*.test.ts` or `*.test.tsx` near the related code when possible. Add tests for user-visible behavior, state transitions, route guards, data transforms, and map/chat interactions. Run `npm test` or `npm run test:coverage` before larger PRs.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
History uses Conventional Commit messages such as `feat(map): add coordinate zoom action` and `fix(chat): hide raw permission metadata`, with occasional Chinese summaries. Prefer `feat(scope):`, `fix(scope):`, or `refactor(scope):`.
|
||||||
|
|
||||||
|
PRs should include a UI/behavior summary, verification commands, screenshots for visual changes, and notes for changed environment variables or backend API expectations.
|
||||||
|
|
||||||
|
## Security & Configuration Tips
|
||||||
|
|
||||||
|
Do not commit `.env`, `.next/`, `node_modules/`, local caches, or private map/API tokens. Public build-time variables should be documented; sensitive values belong in Gitea secrets.
|
||||||
@@ -27,32 +27,6 @@ import VerifiedUserRounded from "@mui/icons-material/VerifiedUserRounded";
|
|||||||
import type { PermissionReply } from "@/lib/chatStream";
|
import type { PermissionReply } from "@/lib/chatStream";
|
||||||
import type { Message } from "./GlobalChatbox.types";
|
import type { Message } from "./GlobalChatbox.types";
|
||||||
|
|
||||||
const formatMetadataValue = (value: unknown) => {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value);
|
|
||||||
} catch {
|
|
||||||
return "[unserializable]";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const truncateText = (value: string, maxLength: number) =>
|
|
||||||
value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
|
|
||||||
|
|
||||||
const formatMetadata = (metadata: Record<string, unknown>) => {
|
|
||||||
const entries = Object.entries(metadata)
|
|
||||||
.filter(([key]) => !["command", "path", "file", "directory"].includes(key))
|
|
||||||
.slice(0, 3);
|
|
||||||
if (!entries.length) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return entries
|
|
||||||
.map(([key, value]) => `${key}: ${truncateText(formatMetadataValue(value), 64)}`)
|
|
||||||
.join(";");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPermissionTitle = (permission: NonNullable<Message["permissions"]>[number]) => {
|
const getPermissionTitle = (permission: NonNullable<Message["permissions"]>[number]) => {
|
||||||
if (permission.permission === "external_directory") return "访问工作区外目录";
|
if (permission.permission === "external_directory") return "访问工作区外目录";
|
||||||
if (permission.permission === "bash") return "执行终端命令";
|
if (permission.permission === "bash") return "执行终端命令";
|
||||||
@@ -63,15 +37,8 @@ const getPermissionTitle = (permission: NonNullable<Message["permissions"]>[numb
|
|||||||
const getPermissionPrimaryValue = (
|
const getPermissionPrimaryValue = (
|
||||||
permission: NonNullable<Message["permissions"]>[number],
|
permission: NonNullable<Message["permissions"]>[number],
|
||||||
) => {
|
) => {
|
||||||
const command = permission.metadata.command;
|
if (typeof permission.target === "string" && permission.target.trim()) {
|
||||||
if (typeof command === "string" && command.trim()) {
|
return permission.target.trim();
|
||||||
return command.trim();
|
|
||||||
}
|
|
||||||
for (const key of ["path", "file", "directory"]) {
|
|
||||||
const value = permission.metadata[key];
|
|
||||||
if (typeof value === "string" && value.trim()) {
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return permission.patterns[0] ?? permission.permission;
|
return permission.patterns[0] ?? permission.permission;
|
||||||
};
|
};
|
||||||
@@ -94,6 +61,7 @@ const getPermissionStatusLabel = (status: NonNullable<Message["permissions"]>[nu
|
|||||||
if (status === "approved_always") return "已始终允许";
|
if (status === "approved_always") return "已始终允许";
|
||||||
if (status === "approved_once") return "已允许一次";
|
if (status === "approved_once") return "已允许一次";
|
||||||
if (status === "rejected") return "已拒绝";
|
if (status === "rejected") return "已拒绝";
|
||||||
|
if (status === "aborted") return "已中断";
|
||||||
if (status === "error") return "提交失败";
|
if (status === "error") return "提交失败";
|
||||||
if (status === "submitting") return "提交中";
|
if (status === "submitting") return "提交中";
|
||||||
return "等待确认";
|
return "等待确认";
|
||||||
@@ -109,6 +77,7 @@ const getPermissionStatusColor = (
|
|||||||
if (status === "approved_once") return approvedOncePermissionColor;
|
if (status === "approved_once") return approvedOncePermissionColor;
|
||||||
if (status === "approved_always") return theme.palette.success.main;
|
if (status === "approved_always") return theme.palette.success.main;
|
||||||
if (status === "rejected" || status === "error") return theme.palette.error.main;
|
if (status === "rejected" || status === "error") return theme.palette.error.main;
|
||||||
|
if (status === "aborted") return theme.palette.text.secondary;
|
||||||
return pendingPermissionColor;
|
return pendingPermissionColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,21 +88,24 @@ const getPermissionStatusTextColor = (
|
|||||||
if (status === "approved_once") return "#006c78";
|
if (status === "approved_once") return "#006c78";
|
||||||
if (status === "approved_always") return theme.palette.success.dark;
|
if (status === "approved_always") return theme.palette.success.dark;
|
||||||
if (status === "rejected" || status === "error") return theme.palette.error.main;
|
if (status === "rejected" || status === "error") return theme.palette.error.main;
|
||||||
|
if (status === "aborted") return theme.palette.text.secondary;
|
||||||
return "#8a5a00";
|
return "#8a5a00";
|
||||||
};
|
};
|
||||||
|
|
||||||
const PermissionRequestCard = ({
|
const PermissionRequestCard = ({
|
||||||
permission,
|
permission,
|
||||||
|
isRunning,
|
||||||
onReply,
|
onReply,
|
||||||
}: {
|
}: {
|
||||||
permission: NonNullable<Message["permissions"]>[number];
|
permission: NonNullable<Message["permissions"]>[number];
|
||||||
|
isRunning: boolean;
|
||||||
onReply: (requestId: string, reply: PermissionReply) => void;
|
onReply: (requestId: string, reply: PermissionReply) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isPending = permission.status === "pending" || permission.status === "error";
|
const isPending =
|
||||||
const isSubmitting = permission.status === "submitting";
|
isRunning && (permission.status === "pending" || permission.status === "error");
|
||||||
|
const isSubmitting = isRunning && permission.status === "submitting";
|
||||||
const primaryValue = getPermissionPrimaryValue(permission);
|
const primaryValue = getPermissionPrimaryValue(permission);
|
||||||
const metadataText = formatMetadata(permission.metadata);
|
|
||||||
const accentColor = getPermissionStatusColor(permission.status, theme);
|
const accentColor = getPermissionStatusColor(permission.status, theme);
|
||||||
const statusTextColor = getPermissionStatusTextColor(permission.status, theme);
|
const statusTextColor = getPermissionStatusTextColor(permission.status, theme);
|
||||||
const statusLabel = getPermissionStatusLabel(permission.status);
|
const statusLabel = getPermissionStatusLabel(permission.status);
|
||||||
@@ -231,12 +203,6 @@ const PermissionRequestCard = ({
|
|||||||
{primaryValue}
|
{primaryValue}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{metadataText ? (
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: "break-word" }}>
|
|
||||||
{metadataText}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{permission.error ? (
|
{permission.error ? (
|
||||||
@@ -363,7 +329,13 @@ export const PermissionRequestGroup = ({
|
|||||||
const onceCount = permissions.filter((permission) => permission.status === "approved_once").length;
|
const onceCount = permissions.filter((permission) => permission.status === "approved_once").length;
|
||||||
const alwaysCount = permissions.filter((permission) => permission.status === "approved_always").length;
|
const alwaysCount = permissions.filter((permission) => permission.status === "approved_always").length;
|
||||||
const rejectedCount = permissions.filter((permission) => permission.status === "rejected").length;
|
const rejectedCount = permissions.filter((permission) => permission.status === "rejected").length;
|
||||||
const pendingCount = permissions.length - onceCount - alwaysCount - rejectedCount;
|
const abortedCount = permissions.filter((permission) => permission.status === "aborted").length;
|
||||||
|
const pendingCount = permissions.filter(
|
||||||
|
(permission) =>
|
||||||
|
permission.status === "pending" ||
|
||||||
|
permission.status === "submitting" ||
|
||||||
|
permission.status === "error",
|
||||||
|
).length;
|
||||||
const hasPendingPermissions = pendingCount > 0;
|
const hasPendingPermissions = pendingCount > 0;
|
||||||
const [expanded, setExpanded] = React.useState(false);
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
const latestPermissions = permissions.slice(-3);
|
const latestPermissions = permissions.slice(-3);
|
||||||
@@ -378,9 +350,24 @@ export const PermissionRequestGroup = ({
|
|||||||
{ label: "允许一次", value: onceCount, color: getPermissionStatusColor("approved_once", theme), textColor: getPermissionStatusTextColor("approved_once", theme) },
|
{ label: "允许一次", value: onceCount, color: getPermissionStatusColor("approved_once", theme), textColor: getPermissionStatusTextColor("approved_once", theme) },
|
||||||
{ label: "始终允许", value: alwaysCount, color: getPermissionStatusColor("approved_always", theme), textColor: getPermissionStatusTextColor("approved_always", theme) },
|
{ label: "始终允许", value: alwaysCount, color: getPermissionStatusColor("approved_always", theme), textColor: getPermissionStatusTextColor("approved_always", theme) },
|
||||||
{ label: "拒绝", value: rejectedCount, color: getPermissionStatusColor("rejected", theme), textColor: getPermissionStatusTextColor("rejected", theme) },
|
{ label: "拒绝", value: rejectedCount, color: getPermissionStatusColor("rejected", theme), textColor: getPermissionStatusTextColor("rejected", theme) },
|
||||||
|
{ label: "中断", value: abortedCount, color: getPermissionStatusColor("aborted", theme), textColor: getPermissionStatusTextColor("aborted", theme) },
|
||||||
];
|
];
|
||||||
const chipColor = pendingCount > 0 ? getPermissionStatusColor("pending", theme) : rejectedCount > 0 ? getPermissionStatusColor("rejected", theme) : getPermissionStatusColor("approved_always", theme);
|
const chipColor =
|
||||||
const chipTextColor = pendingCount > 0 ? getPermissionStatusTextColor("pending", theme) : rejectedCount > 0 ? getPermissionStatusTextColor("rejected", theme) : getPermissionStatusTextColor("approved_always", theme);
|
pendingCount > 0
|
||||||
|
? getPermissionStatusColor("pending", theme)
|
||||||
|
: abortedCount > 0
|
||||||
|
? getPermissionStatusColor("aborted", theme)
|
||||||
|
: rejectedCount > 0
|
||||||
|
? getPermissionStatusColor("rejected", theme)
|
||||||
|
: getPermissionStatusColor("approved_always", theme);
|
||||||
|
const chipTextColor =
|
||||||
|
pendingCount > 0
|
||||||
|
? getPermissionStatusTextColor("pending", theme)
|
||||||
|
: abortedCount > 0
|
||||||
|
? getPermissionStatusTextColor("aborted", theme)
|
||||||
|
: rejectedCount > 0
|
||||||
|
? getPermissionStatusTextColor("rejected", theme)
|
||||||
|
: getPermissionStatusTextColor("approved_always", theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -549,12 +536,13 @@ export const PermissionRequestGroup = ({
|
|||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
noWrap
|
noWrap
|
||||||
|
title={primaryValue}
|
||||||
sx={{
|
sx={{
|
||||||
display: "block",
|
display: "block",
|
||||||
fontFamily: permission.permission === "bash" ? "monospace" : undefined,
|
fontFamily: permission.permission === "bash" ? "monospace" : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{truncateText(primaryValue, 72)}
|
{primaryValue}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Chip
|
<Chip
|
||||||
@@ -591,6 +579,7 @@ export const PermissionRequestGroup = ({
|
|||||||
<PermissionRequestCard
|
<PermissionRequestCard
|
||||||
key={permission.requestId}
|
key={permission.requestId}
|
||||||
permission={permission}
|
permission={permission}
|
||||||
|
isRunning={isRunning}
|
||||||
onReply={onReply}
|
onReply={onReply}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -605,6 +594,7 @@ export const PermissionRequestGroup = ({
|
|||||||
<PermissionRequestCard
|
<PermissionRequestCard
|
||||||
key={permission.requestId}
|
key={permission.requestId}
|
||||||
permission={permission}
|
permission={permission}
|
||||||
|
isRunning={isRunning}
|
||||||
onReply={onReply}
|
onReply={onReply}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -613,5 +603,3 @@ export const PermissionRequestGroup = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import StopRounded from "@mui/icons-material/StopRounded";
|
|||||||
|
|
||||||
type AgentTurnProps = {
|
type AgentTurnProps = {
|
||||||
message: Message;
|
message: Message;
|
||||||
|
isStreaming: boolean;
|
||||||
messageSpeechState: SpeechState;
|
messageSpeechState: SpeechState;
|
||||||
onSpeak: (messageId: string, text: string) => void;
|
onSpeak: (messageId: string, text: string) => void;
|
||||||
onPause: () => void;
|
onPause: () => void;
|
||||||
@@ -54,6 +55,7 @@ type AgentTurnProps = {
|
|||||||
export const AgentTurn = React.memo(
|
export const AgentTurn = React.memo(
|
||||||
({
|
({
|
||||||
message,
|
message,
|
||||||
|
isStreaming,
|
||||||
messageSpeechState,
|
messageSpeechState,
|
||||||
onSpeak,
|
onSpeak,
|
||||||
onPause,
|
onPause,
|
||||||
@@ -277,7 +279,7 @@ export const AgentTurn = React.memo(
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isHovered && (
|
{isHovered && !isStreaming && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.9, y: 5 }}
|
initial={{ opacity: 0, scale: 0.9, y: 5 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type AgentWorkspaceProps = {
|
|||||||
|
|
||||||
type TurnListProps = {
|
type TurnListProps = {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
|
isStreaming: boolean;
|
||||||
speakingMessageId: string | null;
|
speakingMessageId: string | null;
|
||||||
speechState: SpeechState;
|
speechState: SpeechState;
|
||||||
onSpeak: (messageId: string, text: string) => void;
|
onSpeak: (messageId: string, text: string) => void;
|
||||||
@@ -55,6 +56,7 @@ const sameMessages = (left: Message[], right: Message[]) =>
|
|||||||
|
|
||||||
const TurnListInner = ({
|
const TurnListInner = ({
|
||||||
messages,
|
messages,
|
||||||
|
isStreaming,
|
||||||
speakingMessageId,
|
speakingMessageId,
|
||||||
speechState,
|
speechState,
|
||||||
onSpeak,
|
onSpeak,
|
||||||
@@ -73,6 +75,7 @@ const TurnListInner = ({
|
|||||||
<AgentTurn
|
<AgentTurn
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
|
isStreaming={isStreaming}
|
||||||
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
|
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
|
||||||
onSpeak={onSpeak}
|
onSpeak={onSpeak}
|
||||||
onPause={onPauseSpeech}
|
onPause={onPauseSpeech}
|
||||||
@@ -93,6 +96,7 @@ const TurnList = React.memo(
|
|||||||
TurnListInner,
|
TurnListInner,
|
||||||
(prevProps, nextProps) =>
|
(prevProps, nextProps) =>
|
||||||
sameMessages(prevProps.messages, nextProps.messages) &&
|
sameMessages(prevProps.messages, nextProps.messages) &&
|
||||||
|
prevProps.isStreaming === nextProps.isStreaming &&
|
||||||
prevProps.speakingMessageId === nextProps.speakingMessageId &&
|
prevProps.speakingMessageId === nextProps.speakingMessageId &&
|
||||||
prevProps.speechState === nextProps.speechState &&
|
prevProps.speechState === nextProps.speechState &&
|
||||||
prevProps.onSpeak === nextProps.onSpeak &&
|
prevProps.onSpeak === nextProps.onSpeak &&
|
||||||
@@ -274,6 +278,7 @@ export const AgentWorkspace = ({
|
|||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
<TurnList
|
<TurnList
|
||||||
messages={historyMessages}
|
messages={historyMessages}
|
||||||
|
isStreaming={isStreaming}
|
||||||
speakingMessageId={speakingMessageId}
|
speakingMessageId={speakingMessageId}
|
||||||
speechState={speechState}
|
speechState={speechState}
|
||||||
onSpeak={onSpeak}
|
onSpeak={onSpeak}
|
||||||
@@ -290,6 +295,7 @@ export const AgentWorkspace = ({
|
|||||||
{streamingMessage ? (
|
{streamingMessage ? (
|
||||||
<TurnList
|
<TurnList
|
||||||
messages={[streamingMessage]}
|
messages={[streamingMessage]}
|
||||||
|
isStreaming={isStreaming}
|
||||||
speakingMessageId={speakingMessageId}
|
speakingMessageId={speakingMessageId}
|
||||||
speechState={speechState}
|
speechState={speechState}
|
||||||
onSpeak={onSpeak}
|
onSpeak={onSpeak}
|
||||||
|
|||||||
@@ -118,6 +118,12 @@ const TOOL_META: Record<string, ToolMeta> = {
|
|||||||
actionLabel: "定位到地图",
|
actionLabel: "定位到地图",
|
||||||
color: "#3ba272",
|
color: "#3ba272",
|
||||||
},
|
},
|
||||||
|
zoom_to_map: {
|
||||||
|
label: "缩放到坐标",
|
||||||
|
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||||
|
actionLabel: "缩放到地图",
|
||||||
|
color: "#0ea5e9",
|
||||||
|
},
|
||||||
view_history: {
|
view_history: {
|
||||||
label: "查看计算结果",
|
label: "查看计算结果",
|
||||||
icon: <TimelineRounded sx={{ fontSize: 18 }} />,
|
icon: <TimelineRounded sx={{ fontSize: 18 }} />,
|
||||||
@@ -176,6 +182,46 @@ function normalizeLocateIds(params: Record<string, unknown>): string[] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readFiniteNumber(value: unknown): number | null {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "string" && value.trim()) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildZoomTo3857Action(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
): Extract<ChatToolAction, { type: "zoom_to_map" }> | null {
|
||||||
|
const rawCoordinate = params.coordinate ?? params.coordinates ?? params.center;
|
||||||
|
const tuple = Array.isArray(rawCoordinate)
|
||||||
|
? rawCoordinate
|
||||||
|
: [params.x ?? params.lon ?? params.longitude, params.y ?? params.lat ?? params.latitude];
|
||||||
|
const x = readFiniteNumber(tuple[0]);
|
||||||
|
const y = readFiniteNumber(tuple[1]);
|
||||||
|
if (x === null || y === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoom = readFiniteNumber(params.zoom);
|
||||||
|
const durationMs = readFiniteNumber(params.duration_ms ?? params.durationMs);
|
||||||
|
const rawSourceCrs = params.source_crs ?? params.sourceCrs ?? params.crs;
|
||||||
|
const normalizedSourceCrs =
|
||||||
|
typeof rawSourceCrs === "string" ? rawSourceCrs.trim().toUpperCase() : "";
|
||||||
|
const sourceCrs =
|
||||||
|
normalizedSourceCrs === "EPSG:4326" ? "EPSG:4326" : "EPSG:3857";
|
||||||
|
return {
|
||||||
|
type: "zoom_to_map",
|
||||||
|
coordinate: [x, y],
|
||||||
|
sourceCrs,
|
||||||
|
zoom: zoom ?? undefined,
|
||||||
|
durationMs: durationMs ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getToolDescription(toolCall: ToolCall): string {
|
function getToolDescription(toolCall: ToolCall): string {
|
||||||
const { params } = toolCall;
|
const { params } = toolCall;
|
||||||
const resolveScadaFeatureInfos = (): [string, string][] => {
|
const resolveScadaFeatureInfos = (): [string, string][] => {
|
||||||
@@ -281,6 +327,14 @@ function getToolDescription(toolCall: ToolCall): string {
|
|||||||
case "render_junctions": {
|
case "render_junctions": {
|
||||||
return (params.render_ref as string | undefined) ?? "渲染引用";
|
return (params.render_ref as string | undefined) ?? "渲染引用";
|
||||||
}
|
}
|
||||||
|
case "zoom_to_map": {
|
||||||
|
const action = buildZoomTo3857Action(params);
|
||||||
|
if (!action) {
|
||||||
|
return "地图坐标";
|
||||||
|
}
|
||||||
|
const zoom = action.zoom === undefined ? "" : ` · zoom ${action.zoom}`;
|
||||||
|
return `${action.coordinate[0]}, ${action.coordinate[1]} · ${action.sourceCrs}${zoom}`;
|
||||||
|
}
|
||||||
case APPLY_LAYER_STYLE_TOOL: {
|
case APPLY_LAYER_STYLE_TOOL: {
|
||||||
const payload = parseApplyLayerStylePayload(params);
|
const payload = parseApplyLayerStylePayload(params);
|
||||||
return payload ? describeApplyLayerStyle(payload) : "图层样式";
|
return payload ? describeApplyLayerStyle(payload) : "图层样式";
|
||||||
@@ -341,6 +395,8 @@ function buildAction(toolCall: ToolCall): ChatToolAction | null {
|
|||||||
(params.end as string | undefined),
|
(params.end as string | undefined),
|
||||||
});
|
});
|
||||||
switch (toolCall.tool) {
|
switch (toolCall.tool) {
|
||||||
|
case "zoom_to_map":
|
||||||
|
return buildZoomTo3857Action(params);
|
||||||
case "locate_features": {
|
case "locate_features": {
|
||||||
const featureTypeRaw = params.feature_type;
|
const featureTypeRaw = params.feature_type;
|
||||||
const featureType =
|
const featureType =
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export type AgentPermissionStatus =
|
|||||||
| "approved_once"
|
| "approved_once"
|
||||||
| "approved_always"
|
| "approved_always"
|
||||||
| "rejected"
|
| "rejected"
|
||||||
|
| "aborted"
|
||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
export type AgentPermissionRequest = {
|
export type AgentPermissionRequest = {
|
||||||
@@ -40,7 +41,7 @@ export type AgentPermissionRequest = {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
patterns: string[];
|
patterns: string[];
|
||||||
metadata: Record<string, unknown>;
|
target?: string;
|
||||||
always: string[];
|
always: string[];
|
||||||
tool?: {
|
tool?: {
|
||||||
messageID: string;
|
messageID: string;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { cloneMessage } from "./GlobalChatbox.utils";
|
||||||
|
import type { Message } from "./GlobalChatbox.types";
|
||||||
|
|
||||||
|
describe("cloneMessage", () => {
|
||||||
|
it("normalizes persisted question and todo arrays", () => {
|
||||||
|
const message = {
|
||||||
|
id: "assistant-1",
|
||||||
|
role: "assistant",
|
||||||
|
content: "需要补充信息",
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
requestId: "question-1",
|
||||||
|
sessionId: "session-1",
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
header: "范围",
|
||||||
|
question: "请选择分析范围",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createdAt: 1,
|
||||||
|
status: "pending",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
todos: {
|
||||||
|
sessionId: "session-1",
|
||||||
|
createdAt: 1,
|
||||||
|
},
|
||||||
|
} as unknown as Message;
|
||||||
|
|
||||||
|
const cloned = cloneMessage(message);
|
||||||
|
|
||||||
|
expect(cloned.questions?.[0]?.questions[0]?.options).toEqual([]);
|
||||||
|
expect(cloned.todos?.todos).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { Message } from "./GlobalChatbox.types";
|
import type { Message } from "./GlobalChatbox.types";
|
||||||
|
import type {
|
||||||
|
AgentQuestionRequest,
|
||||||
|
AgentTodoUpdate,
|
||||||
|
} from "@/lib/chatStream";
|
||||||
|
|
||||||
export const createId = () =>
|
export const createId = () =>
|
||||||
`${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
`${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
@@ -29,10 +33,65 @@ export const stripMarkdown = (md: string): string =>
|
|||||||
.replace(/<[^>]+>/g, "")
|
.replace(/<[^>]+>/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
const normalizeQuestionRequests = (
|
||||||
|
questions: Message["questions"],
|
||||||
|
): Message["questions"] =>
|
||||||
|
Array.isArray(questions)
|
||||||
|
? questions.map((request) => ({
|
||||||
|
...request,
|
||||||
|
questions: Array.isArray(request.questions)
|
||||||
|
? request.questions.map((question) => ({
|
||||||
|
...question,
|
||||||
|
header: typeof question.header === "string" ? question.header : "",
|
||||||
|
question:
|
||||||
|
typeof question.question === "string" ? question.question : "",
|
||||||
|
options: Array.isArray(question.options)
|
||||||
|
? question.options.map((option) => ({
|
||||||
|
label:
|
||||||
|
typeof option.label === "string" ? option.label : "",
|
||||||
|
description:
|
||||||
|
typeof option.description === "string"
|
||||||
|
? option.description
|
||||||
|
: "",
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
answers: Array.isArray(request.answers)
|
||||||
|
? request.answers.map((answer) =>
|
||||||
|
Array.isArray(answer)
|
||||||
|
? answer.filter((item): item is string => typeof item === "string")
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
} satisfies AgentQuestionRequest))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const normalizeTodoUpdate = (todos: Message["todos"]): Message["todos"] => {
|
||||||
|
if (!todos) return undefined;
|
||||||
|
return {
|
||||||
|
...todos,
|
||||||
|
todos: Array.isArray(todos.todos)
|
||||||
|
? todos.todos.map((todo) => ({ ...todo }))
|
||||||
|
: [],
|
||||||
|
} satisfies AgentTodoUpdate;
|
||||||
|
};
|
||||||
|
|
||||||
export const cloneMessage = (message: Message): Message => ({
|
export const cloneMessage = (message: Message): Message => ({
|
||||||
...message,
|
...message,
|
||||||
progress: message.progress ? [...message.progress] : undefined,
|
progress: Array.isArray(message.progress) ? [...message.progress] : undefined,
|
||||||
artifacts: message.artifacts ? [...message.artifacts] : undefined,
|
artifacts: Array.isArray(message.artifacts) ? [...message.artifacts] : undefined,
|
||||||
|
permissions: Array.isArray(message.permissions)
|
||||||
|
? message.permissions.map((permission) => ({
|
||||||
|
...permission,
|
||||||
|
patterns: Array.isArray(permission.patterns)
|
||||||
|
? [...permission.patterns]
|
||||||
|
: [],
|
||||||
|
always: Array.isArray(permission.always) ? [...permission.always] : [],
|
||||||
|
}))
|
||||||
|
: undefined,
|
||||||
|
questions: normalizeQuestionRequests(message.questions),
|
||||||
|
todos: normalizeTodoUpdate(message.todos),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cloneMessages = (messages: Message[]) => messages.map(cloneMessage);
|
export const cloneMessages = (messages: Message[]) => messages.map(cloneMessage);
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const upsertPermission = (
|
|||||||
sessionId: event.sessionId,
|
sessionId: event.sessionId,
|
||||||
permission: event.permission,
|
permission: event.permission,
|
||||||
patterns: event.patterns,
|
patterns: event.patterns,
|
||||||
metadata: event.metadata,
|
target: event.target,
|
||||||
always: event.always,
|
always: event.always,
|
||||||
tool: event.tool,
|
tool: event.tool,
|
||||||
createdAt: event.createdAt,
|
createdAt: event.createdAt,
|
||||||
@@ -364,7 +364,7 @@ export const normalizeSessionTodos = (
|
|||||||
return changed ? nextMessages : messages;
|
return changed ? nextMessages : messages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rejectOpenPermissionsAfterAbort = (
|
export const abortOpenPermissionsAfterAbort = (
|
||||||
permissions: AgentPermissionRequest[] | undefined,
|
permissions: AgentPermissionRequest[] | undefined,
|
||||||
) => {
|
) => {
|
||||||
if (!permissions?.length) return permissions;
|
if (!permissions?.length) return permissions;
|
||||||
@@ -380,7 +380,7 @@ export const rejectOpenPermissionsAfterAbort = (
|
|||||||
changed = true;
|
changed = true;
|
||||||
return {
|
return {
|
||||||
...permission,
|
...permission,
|
||||||
status: "rejected" as const,
|
status: "aborted" as const,
|
||||||
repliedAt: Date.now(),
|
repliedAt: Date.now(),
|
||||||
error: undefined,
|
error: undefined,
|
||||||
};
|
};
|
||||||
@@ -415,12 +415,12 @@ export const rejectOpenQuestionsAfterAbort = (
|
|||||||
export const finalizeAssistantMessageAfterAbort = (message: Message): Message => {
|
export const finalizeAssistantMessageAfterAbort = (message: Message): Message => {
|
||||||
const completedProgress = completeRunningProgress(message.progress);
|
const completedProgress = completeRunningProgress(message.progress);
|
||||||
const cancelledTodos = cancelRunningTodos(message.todos);
|
const cancelledTodos = cancelRunningTodos(message.todos);
|
||||||
const rejectedPermissions = rejectOpenPermissionsAfterAbort(message.permissions);
|
const abortedPermissions = abortOpenPermissionsAfterAbort(message.permissions);
|
||||||
const rejectedQuestions = rejectOpenQuestionsAfterAbort(message.questions);
|
const rejectedQuestions = rejectOpenQuestionsAfterAbort(message.questions);
|
||||||
const hasVisibleOutput =
|
const hasVisibleOutput =
|
||||||
message.content.trim().length > 0 ||
|
message.content.trim().length > 0 ||
|
||||||
Boolean(message.artifacts?.length) ||
|
Boolean(message.artifacts?.length) ||
|
||||||
Boolean(rejectedPermissions?.length) ||
|
Boolean(abortedPermissions?.length) ||
|
||||||
Boolean(rejectedQuestions?.length) ||
|
Boolean(rejectedQuestions?.length) ||
|
||||||
Boolean(completedProgress?.length) ||
|
Boolean(completedProgress?.length) ||
|
||||||
Boolean(cancelledTodos);
|
Boolean(cancelledTodos);
|
||||||
@@ -434,7 +434,7 @@ export const finalizeAssistantMessageAfterAbort = (message: Message): Message =>
|
|||||||
content: message.content || "⚠️ **请求已中断**",
|
content: message.content || "⚠️ **请求已中断**",
|
||||||
isError: true,
|
isError: true,
|
||||||
progress: completedProgress,
|
progress: completedProgress,
|
||||||
permissions: rejectedPermissions,
|
permissions: abortedPermissions,
|
||||||
questions: rejectedQuestions,
|
questions: rejectedQuestions,
|
||||||
todos: cancelledTodos,
|
todos: cancelledTodos,
|
||||||
};
|
};
|
||||||
@@ -454,4 +454,3 @@ export const createAssistantMessage = (): Message => ({
|
|||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "",
|
content: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ describe("useAgentChatSession actions", () => {
|
|||||||
requestId: "perm-1",
|
requestId: "perm-1",
|
||||||
permission: "bash",
|
permission: "bash",
|
||||||
patterns: ["rm *"],
|
patterns: ["rm *"],
|
||||||
metadata: { command: "rm tmp.txt" },
|
target: "rm tmp.txt",
|
||||||
always: ["rm *"],
|
always: ["rm *"],
|
||||||
createdAt: 123,
|
createdAt: 123,
|
||||||
});
|
});
|
||||||
@@ -163,7 +163,7 @@ describe("useAgentChatSession actions", () => {
|
|||||||
requestId: "perm-abort",
|
requestId: "perm-abort",
|
||||||
permission: "bash",
|
permission: "bash",
|
||||||
patterns: ["npm test"],
|
patterns: ["npm test"],
|
||||||
metadata: { command: "npm test" },
|
target: "npm test",
|
||||||
always: ["npm test"],
|
always: ["npm test"],
|
||||||
createdAt: 1002,
|
createdAt: 1002,
|
||||||
} satisfies StreamEvent);
|
} satisfies StreamEvent);
|
||||||
@@ -238,7 +238,7 @@ describe("useAgentChatSession actions", () => {
|
|||||||
permissions: [
|
permissions: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
requestId: "perm-abort",
|
requestId: "perm-abort",
|
||||||
status: "rejected",
|
status: "aborted",
|
||||||
repliedAt: expect.any(Number),
|
repliedAt: expect.any(Number),
|
||||||
error: undefined,
|
error: undefined,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -148,6 +148,46 @@ const compactNames = (names: string[]) => {
|
|||||||
: names.join(", ");
|
: names.join(", ");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readFiniteNumber = (value: unknown): number | null => {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "string" && value.trim()) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseZoomTo3857Action = (
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
): Extract<ChatToolAction, { type: "zoom_to_map" }> | null => {
|
||||||
|
const rawCoordinate = params.coordinate ?? params.coordinates ?? params.center;
|
||||||
|
const tuple = Array.isArray(rawCoordinate)
|
||||||
|
? rawCoordinate
|
||||||
|
: [params.x ?? params.lon ?? params.longitude, params.y ?? params.lat ?? params.latitude];
|
||||||
|
const x = readFiniteNumber(tuple[0]);
|
||||||
|
const y = readFiniteNumber(tuple[1]);
|
||||||
|
if (x === null || y === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoom = readFiniteNumber(params.zoom);
|
||||||
|
const durationMs = readFiniteNumber(params.duration_ms ?? params.durationMs);
|
||||||
|
const rawSourceCrs = params.source_crs ?? params.sourceCrs ?? params.crs;
|
||||||
|
const normalizedSourceCrs =
|
||||||
|
typeof rawSourceCrs === "string" ? rawSourceCrs.trim().toUpperCase() : "";
|
||||||
|
const sourceCrs =
|
||||||
|
normalizedSourceCrs === "EPSG:4326" ? "EPSG:4326" : "EPSG:3857";
|
||||||
|
return {
|
||||||
|
type: "zoom_to_map",
|
||||||
|
coordinate: [x, y],
|
||||||
|
sourceCrs,
|
||||||
|
zoom: zoom ?? undefined,
|
||||||
|
durationMs: durationMs ?? undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const buildLocateArtifact = (
|
const buildLocateArtifact = (
|
||||||
tool: string,
|
tool: string,
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
@@ -190,6 +230,18 @@ const buildToolAction = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tool === "zoom_to_map") {
|
||||||
|
const action = parseZoomTo3857Action(params);
|
||||||
|
return {
|
||||||
|
action,
|
||||||
|
kind: "map",
|
||||||
|
title: "缩放到地图坐标",
|
||||||
|
description: action
|
||||||
|
? `${action.coordinate[0]}, ${action.coordinate[1]} (${action.sourceCrs})`
|
||||||
|
: "地图坐标",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (tool === "locate_features" || LOCATE_TOOL_CONFIG[tool]) {
|
if (tool === "locate_features" || LOCATE_TOOL_CONFIG[tool]) {
|
||||||
const locate = buildLocateArtifact(tool, params);
|
const locate = buildLocateArtifact(tool, params);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, type Dispatch, type SetStateAction } fr
|
|||||||
import Feature from "ol/Feature";
|
import Feature from "ol/Feature";
|
||||||
import { GeoJSON } from "ol/format";
|
import { GeoJSON } from "ol/format";
|
||||||
import Point from "ol/geom/Point";
|
import Point from "ol/geom/Point";
|
||||||
|
import { transform } from "ol/proj";
|
||||||
import { bbox, featureCollection } from "@turf/turf";
|
import { bbox, featureCollection } from "@turf/turf";
|
||||||
|
|
||||||
import { useChatToolActionHandler } from "@/hooks/useChatToolActionHandler";
|
import { useChatToolActionHandler } from "@/hooks/useChatToolActionHandler";
|
||||||
@@ -110,6 +111,18 @@ export const useToolbarChatActions = ({
|
|||||||
locateFeatures(action.ids, action.layer, action.geometryKind);
|
locateFeatures(action.ids, action.layer, action.geometryKind);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "zoom_to_map": {
|
||||||
|
const center =
|
||||||
|
action.sourceCrs === "EPSG:4326"
|
||||||
|
? transform(action.coordinate, "EPSG:4326", "EPSG:3857")
|
||||||
|
: action.coordinate;
|
||||||
|
map?.getView().animate({
|
||||||
|
center,
|
||||||
|
zoom: action.zoom ?? map.getView().getZoom() ?? 18,
|
||||||
|
duration: action.durationMs ?? 1000,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "view_history": {
|
case "view_history": {
|
||||||
setChatPanelFeatureInfos(action.featureInfos);
|
setChatPanelFeatureInfos(action.featureInfos);
|
||||||
setChatPanelType(action.dataType);
|
setChatPanelType(action.dataType);
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ describe("streamAgentChat", () => {
|
|||||||
apiFetch.mockResolvedValue({
|
apiFetch.mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
body: makeStream([
|
body: makeStream([
|
||||||
'event: permission_request\ndata: {"session_id":"s1","request_id":"perm-1","permission":"bash","patterns":["rm *"],"metadata":{"command":"rm tmp.txt"},"always":["rm *"],"created_at":123}\n\n',
|
'event: permission_request\ndata: {"session_id":"s1","request_id":"perm-1","permission":"bash","patterns":["rm *"],"target":"rm tmp.txt","always":["rm *"],"created_at":123}\n\n',
|
||||||
'event: permission_response\ndata: {"session_id":"s1","request_id":"perm-1","reply":"reject"}\n\n',
|
'event: permission_response\ndata: {"session_id":"s1","request_id":"perm-1","reply":"reject"}\n\n',
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
@@ -205,7 +205,7 @@ describe("streamAgentChat", () => {
|
|||||||
requestId: "perm-1",
|
requestId: "perm-1",
|
||||||
permission: "bash",
|
permission: "bash",
|
||||||
patterns: ["rm *"],
|
patterns: ["rm *"],
|
||||||
metadata: { command: "rm tmp.txt" },
|
target: "rm tmp.txt",
|
||||||
always: ["rm *"],
|
always: ["rm *"],
|
||||||
tool: undefined,
|
tool: undefined,
|
||||||
createdAt: 123,
|
createdAt: 123,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export type StreamEvent =
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
patterns: string[];
|
patterns: string[];
|
||||||
metadata: Record<string, unknown>;
|
target?: string;
|
||||||
always: string[];
|
always: string[];
|
||||||
tool?: {
|
tool?: {
|
||||||
messageID: string;
|
messageID: string;
|
||||||
@@ -296,7 +296,7 @@ const emitParsedStreamEvent = (
|
|||||||
request_id?: string;
|
request_id?: string;
|
||||||
permission?: string;
|
permission?: string;
|
||||||
patterns?: unknown;
|
patterns?: unknown;
|
||||||
metadata?: unknown;
|
target?: string;
|
||||||
always?: unknown;
|
always?: unknown;
|
||||||
created_at?: number;
|
created_at?: number;
|
||||||
reply?: PermissionReply;
|
reply?: PermissionReply;
|
||||||
@@ -370,7 +370,7 @@ const emitParsedStreamEvent = (
|
|||||||
patterns: Array.isArray(parsed.patterns)
|
patterns: Array.isArray(parsed.patterns)
|
||||||
? parsed.patterns.filter((item): item is string => typeof item === "string")
|
? parsed.patterns.filter((item): item is string => typeof item === "string")
|
||||||
: [],
|
: [],
|
||||||
metadata: isObjectRecord(parsed.metadata) ? parsed.metadata : {},
|
target: typeof parsed.target === "string" ? parsed.target : undefined,
|
||||||
always: Array.isArray(parsed.always)
|
always: Array.isArray(parsed.always)
|
||||||
? parsed.always.filter((item): item is string => typeof item === "string")
|
? parsed.always.filter((item): item is string => typeof item === "string")
|
||||||
: [],
|
: [],
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ export type ChatToolAction =
|
|||||||
layer: string;
|
layer: string;
|
||||||
geometryKind: "point" | "line";
|
geometryKind: "point" | "line";
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "zoom_to_map";
|
||||||
|
coordinate: [number, number];
|
||||||
|
sourceCrs?: "EPSG:3857" | "EPSG:4326";
|
||||||
|
zoom?: number;
|
||||||
|
durationMs?: number;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: "view_history";
|
type: "view_history";
|
||||||
featureInfos: [string, string][];
|
featureInfos: [string, string][];
|
||||||
|
|||||||
Reference in New Issue
Block a user