Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions app/components/editor/assistant-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,9 @@ function CodeDiff({ change }: { change: CodeChange }) {
});
}

/** A question for the assistant from elsewhere in the editor. */
export interface AskAssistant {
text: string;
at: number;
/** The conversation it was asked in. */
thread: string;
}

export function AssistantTab({
ask,
onAccepted
}: {
ask?: AskAssistant;
/** Once the last card is decided, if any was accepted: run and save, as the Save button does. */
onAccepted?: () => void;
}) {
Expand All @@ -160,8 +150,16 @@ export function AssistantTab({
setBoundingBox,
setPropertyFilters
} = useEditorStore();
const { items, setItems, busy, setBusy, setUnsaved, reset } =
useAssistantStore();
const {
items,
setItems,
busy,
setBusy,
setUnsaved,
reset,
question,
takeQuestion
} = useAssistantStore();
const [draft, setDraft] = useState('');
const sentCode = useRef('');
/** The run in flight, which its code cards name. */
Expand Down Expand Up @@ -517,15 +515,15 @@ export function AssistantTab({
item.kind === 'proposal' && item.status === 'pending' && !item.held
);

/** The last question from elsewhere that was sent. */
const asked = useRef<number>(undefined);
useEffect(() => {
// A question asked during a run waits for the run to end. One asked in
// another conversation is about another project: it is dropped.
if (!ask || busy || asked.current === ask.at) return;
asked.current = ask.at;
if (ask.thread === useAssistantStore.getState().threadId) send(ask.text);
}, [ask?.at, busy]);
if (!question || busy) return;
const taken = takeQuestion();
if (taken?.thread === useAssistantStore.getState().threadId) {
send(taken.text);
}
}, [question?.at, busy]);

useEffect(() => {
end.current?.scrollIntoView({ block: 'end' });
Expand Down
24 changes: 12 additions & 12 deletions app/components/editor/editor-workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useCodeEditor } from './code-editor';
import { useCodeExecution } from './use-code-execution';
import { ConfigurationTab } from './configuration-tab';
import { CodeTab } from './code-tab';
import { AssistantTab, type AskAssistant } from './assistant-tab';
import { AssistantTab } from './assistant-tab';
import { appConfig } from '$config/runtime';
import { ExecutionErrorAlert } from './execution-error-alert';
import { EditorHeader } from './editor-header';
Expand Down Expand Up @@ -64,7 +64,11 @@ export function EditorWorkspace({

const [activeTab, setActiveTab] = useState<EditorTabId>(defaultTab);
const [isErrorDismissed, setIsErrorDismissed] = useState(false);
const [ask, setAsk] = useState<AskAssistant>();
// A question from an error, here or on the map, opens the Assistant tab.
const question = useAssistantStore((state) => state.question);
useEffect(() => {
if (question) setActiveTab('assistant');
}, [question?.at]);
// Set when the assistant's change is accepted; the save waits for the render that carries the
// accepted configuration, which the execution hook reads from props.
const [saveAfterAccept, setSaveAfterAccept] = useState(false);
Expand Down Expand Up @@ -219,10 +223,7 @@ export function EditorWorkspace({
p={4}
bg='bg'
>
<AssistantTab
ask={ask}
onAccepted={() => setSaveAfterAccept(true)}
/>
<AssistantTab onAccepted={() => setSaveAfterAccept(true)} />
</Tabs.Content>
</Stack>

Expand All @@ -233,12 +234,11 @@ export function EditorWorkspace({
onAsk={
appConfig.agentUrl
? () => {
setAsk({
text: `My code failed to run. Why, and how do I fix it?\n\n${useEditorStore.getState().lastRun?.error ?? errorMessage}`,
at: Date.now(),
thread: useAssistantStore.getState().threadId
});
setActiveTab('assistant');
useAssistantStore
.getState()
.ask(
`My code failed to run. Why, and how do I fix it?\n\n${useEditorStore.getState().lastRun?.error ?? errorMessage}`
);
setIsErrorDismissed(true);
}
: undefined
Expand Down
34 changes: 32 additions & 2 deletions app/components/editor/results-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { LuCodeXml, LuDownload, LuMap, LuWorkflow } from 'react-icons/lu';

import type { ServiceInfo } from '$types';
import type { ProcessGraph } from '$types/openeo-process';
import { appConfig } from '$config/runtime';
import { useAssistantStore } from '$stores/assistant-store';
import { useEditorStore } from '$stores/editor-store';
import { MapViewer } from '$components/map/map-viewer';
import { TileStatusAlert } from '$components/map/tile-status-alert';
Expand All @@ -46,7 +48,8 @@ function ResultsPanelComponent() {
services: state.services
}))
);
const { toggleServiceVisibility, setBoundingBox } = useEditorStore();
const { toggleServiceVisibility, setBoundingBox, setTileError } =
useEditorStore();

const [tileStatus, setTileStatus] = useState<TileLoadStatus>({
pending: 0,
Expand All @@ -62,6 +65,19 @@ function ResultsPanelComponent() {
}
}, [services.length, tileStatus.status]);

// The assistant reads the tile error of the view from the store. A new view
// or a new run sets the status again, so an old error goes with it.
const tileError =
tileStatus.status === 'error' ? tileStatus.error : undefined;
const failed = tileStatus.failed ?? 1;
useEffect(() => {
setTileError(
tileError
? { code: tileError.code, message: tileError.message, failed }
: null
);
}, [tileError?.code, tileError?.message, failed, setTileError]);

return (
<Stack gap={0} flexGrow={1} h='100%' position='relative' asChild>
<Tabs.Root defaultValue='map' variant='subtle' size='sm' lazyMount>
Expand Down Expand Up @@ -121,7 +137,21 @@ function ResultsPanelComponent() {
onServicePublish={(v) => setShareService(v)}
/>
</Flex>
{services.length > 0 && <TileStatusAlert status={tileStatus} />}
{services.length > 0 && (
<TileStatusAlert
status={tileStatus}
onAsk={
appConfig.agentUrl && tileError
? () =>
useAssistantStore
.getState()
.ask(
`My map shows tile errors. Why, and how do I fix it?\n\n${failed} tile(s) failed: ${tileError.code}: ${tileError.message}`
)
: undefined
}
/>
)}

{shareService && (
<ShareDialog
Expand Down
11 changes: 9 additions & 2 deletions app/components/map/tile-status-alert.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Alert, ProgressCircle, Box, Text } from '@chakra-ui/react';
import { Alert, ProgressCircle, Box, Button, Text } from '@chakra-ui/react';
import type { TileLoadStatus } from './use-map-tile-status';

interface TileStatusAlertProps {
status: TileLoadStatus;
/** Asks the assistant about the tile error. Absent when no assistant is configured. */
onAsk?: () => void;
}

export function TileStatusAlert({ status }: TileStatusAlertProps) {
export function TileStatusAlert({ status, onAsk }: TileStatusAlertProps) {
if (status.status === 'idle') return null;

if (status.status === 'error' && status.error) {
Expand All @@ -25,6 +27,11 @@ export function TileStatusAlert({ status }: TileStatusAlertProps) {
{error.message}
</Text>
</Alert.Description>
{onAsk && (
<Button size='xs' variant='outline' mt={2} onClick={onAsk}>
Ask the assistant
</Button>
)}
</Alert.Content>
</Alert.Root>
</Box>
Expand Down
26 changes: 25 additions & 1 deletion app/stores/assistant-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export type Item =
change: CodeChange | ConfigurationChange;
};

/** A question for the assistant from elsewhere in the editor: an error. */
export interface AskAssistant {
text: string;
at: number;
/** The conversation it was asked in. */
thread: string;
}

/**
* The proposals still waiting for a decision, superseded: the user asked
* again instead of deciding them. Accepting one later would apply what the
Expand Down Expand Up @@ -68,12 +76,18 @@ type AssistantState = {
unsaved: boolean;
/** The saved project the conversation is about; null for an unsaved one. */
project: string | null;
/** The last question asked from elsewhere in the editor. Not persisted. */
question: AskAssistant | null;
};

type AssistantActions = {
setItems: (change: (items: Item[]) => Item[]) => void;
setBusy: (busy: boolean) => void;
setUnsaved: (unsaved: boolean) => void;
/** Asks the assistant from elsewhere in the editor: the tab sends it. */
ask: (text: string) => void;
/** The question waiting to be sent, which is then no longer waiting. */
takeQuestion: () => AskAssistant | null;
/** A new conversation about the same project. */
reset: () => void;
/** The project changed: a new conversation, unless it is this one's. */
Expand All @@ -87,7 +101,8 @@ const fresh = () => ({
threadId: crypto.randomUUID(),
items: [],
busy: false,
unsaved: false
unsaved: false,
question: null
});

export const useAssistantStore = create<AssistantState & AssistantActions>()(
Expand All @@ -98,6 +113,15 @@ export const useAssistantStore = create<AssistantState & AssistantActions>()(
setItems: (change) => set((state) => ({ items: change(state.items) })),
setBusy: (busy) => set({ busy }),
setUnsaved: (unsaved) => set({ unsaved }),
ask: (text) =>
set((state) => ({
question: { text, at: Date.now(), thread: state.threadId }
})),
takeQuestion: () => {
const { question } = get();
set({ question: null });
return question;
},
reset: () => set(fresh()),
forProject: (project) => {
if (project !== null && get().project === project) return;
Expand Down
23 changes: 23 additions & 0 deletions app/stores/editor-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ export type LastRun = {
at?: string;
};

/** The last tile error of the map view, for the assistant. Not persisted. */
export type TileError = {
code: string;
message: string;
/** The number of tiles that failed in the view. */
failed: number;
/** When it was seen, as ISO 8601; stamped by setTileError. */
at?: string;
};

type ConfigValues = EditorConfigValues;

const DEFAULT_SCENE_NAME = 'New Project';
Expand All @@ -34,6 +44,7 @@ type EditorState = {
sceneId: string | null;
sceneName: string;
lastRun: LastRun | null;
tileError: TileError | null;
/** Counts the projects opened: a run of an earlier one must not set
* `lastRun`. Not persisted. */
opened: number;
Expand All @@ -44,6 +55,7 @@ type ResetDefaults = Partial<ConfigValues>;
type EditorActions = {
setCode: (code: string) => void;
setLastRun: (lastRun: LastRun | null) => void;
setTileError: (tileError: TileError | null) => void;
setCollectionId: (id: string) => void;
setTemporalRange: (range: [string, string]) => void;
setPropertyFilters: (filters: PropertyFilter[]) => void;
Expand Down Expand Up @@ -89,8 +101,16 @@ export const useEditorStore = create<EditorStore>()(
sceneId: null,
sceneName: DEFAULT_SCENE_NAME,
lastRun: null,
tileError: null,
opened: 0,
setCode: (code) => set((state) => ({ code, ...changed(state) })),
setTileError: (tileError) =>
set({
tileError: tileError && {
...tileError,
at: new Date().toISOString()
}
}),
setLastRun: (lastRun) =>
set({
lastRun: lastRun && { ...lastRun, at: new Date().toISOString() }
Expand Down Expand Up @@ -150,6 +170,7 @@ export const useEditorStore = create<EditorStore>()(
selectedConfig: newConfig,
services: [],
lastRun: null,
tileError: null,
opened: state.opened + 1,
sceneName: DEFAULT_SCENE_NAME
}));
Expand All @@ -163,6 +184,7 @@ export const useEditorStore = create<EditorStore>()(
selectedConfig: initialConfig,
services: [],
lastRun: null,
tileError: null,
opened: state.opened + 1,
sceneId: null,
sceneName: DEFAULT_SCENE_NAME
Expand All @@ -185,6 +207,7 @@ export const useEditorStore = create<EditorStore>()(
isDirty: false,
services: [],
lastRun: null,
tileError: null,
opened: state.opened + 1
}));
}
Expand Down
26 changes: 26 additions & 0 deletions app/types/agent-contract.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@
"additionalProperties": false,
"description": "One error from the openEO `/validation` endpoint."
},
"TileDiagnostic": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "The openEO error code, for example `NoDataAvailable`."
},
"message": { "type": "string" },
"failed": {
"type": "number",
"description": "The number of tiles that failed in the view."
},
"at": { "type": "string" }
},
"required": ["code", "message", "failed", "at"],
"additionalProperties": false,
"description": "The last tile error of the current map view, as the backend sent it: the run built a valid graph, but the backend could not compute a tile. Since contract 1.1."
},
"StudioDiagnostics": {
"type": "object",
"properties": {
Expand All @@ -102,6 +120,13 @@
"lint": {
"type": "array",
"items": { "$ref": "#/definitions/LintDiagnostic" }
},
"tiles": {
"anyOf": [
{ "$ref": "#/definitions/TileDiagnostic" },
{ "type": "null" }
],
"description": "Since contract 1.1."
}
},
"required": ["execution", "validation", "lint"],
Expand Down Expand Up @@ -348,6 +373,7 @@
"editor.executionError",
"editor.validationError",
"editor.lintDiagnostic",
"map.tileError",
"home.kickstart"
]
},
Expand Down
Loading
Loading