Spaces:
Sleeping
Sleeping
harryagasi commited on
Commit ·
3baad7e
1
Parent(s): b5e46e4
[NOTICKET] feat(knowledge,analysis): add deletion confirmation, duplicate-name guard, and unusable-analysis lockout
Browse files- Require confirmation with an impact preview (affected analyses) before
deleting a knowledge source or clearing all documents in KnowledgeManagement
- Block creating analyses with a name that duplicates an existing one
(case/whitespace-insensitive) in NewAnalysisDialog
- Derive an "unusable" state from the existing staleSources check and disable
chat input/send/help, suggested questions, generate-report, and the
source-rebind controls in AnalysisHeader once a bound source is deleted,
while keeping chat history and reports viewable
- .specify/feature.json +3 -1
- CLAUDE.md +2 -2
- specs/003-knowledge-delete-guardrails/checklists/requirements.md +35 -0
- specs/003-knowledge-delete-guardrails/contracts/components.md +119 -0
- specs/003-knowledge-delete-guardrails/data-model.md +49 -0
- specs/003-knowledge-delete-guardrails/plan.md +80 -0
- specs/003-knowledge-delete-guardrails/quickstart.md +42 -0
- specs/003-knowledge-delete-guardrails/research.md +32 -0
- specs/003-knowledge-delete-guardrails/spec.md +124 -0
- specs/003-knowledge-delete-guardrails/tasks.md +182 -0
- src/app/components/KnowledgeManagement.tsx +143 -5
- src/app/components/analysis/AnalysisHeader.tsx +28 -17
- src/app/components/analysis/AnalysisShell.tsx +27 -5
- src/app/components/analysis/NewAnalysisDialog.tsx +19 -2
- src/app/components/analysis/ReportSidebar.tsx +3 -2
- src/app/components/analysis/SuggestedQuestionsBar.tsx +5 -3
.specify/feature.json
CHANGED
|
@@ -1 +1,3 @@
|
|
| 1 |
-
{
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"feature_directory": "specs/003-knowledge-delete-guardrails"
|
| 3 |
+
}
|
CLAUDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
<!-- SPECKIT START -->
|
| 2 |
For additional context about technologies to be used, project structure,
|
| 3 |
shell commands, and other important information, see:
|
| 4 |
-
- Implementation Plan: specs/
|
| 5 |
-
- Feature Specification: specs/
|
| 6 |
<!-- SPECKIT END -->
|
|
|
|
| 1 |
<!-- SPECKIT START -->
|
| 2 |
For additional context about technologies to be used, project structure,
|
| 3 |
shell commands, and other important information, see:
|
| 4 |
+
- Implementation Plan: specs/003-knowledge-delete-guardrails/plan.md
|
| 5 |
+
- Feature Specification: specs/003-knowledge-delete-guardrails/spec.md
|
| 6 |
<!-- SPECKIT END -->
|
specs/003-knowledge-delete-guardrails/checklists/requirements.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Specification Quality Checklist: Knowledge Source Deletion Guardrails
|
| 2 |
+
|
| 3 |
+
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
| 4 |
+
**Created**: 2026-07-24
|
| 5 |
+
**Feature**: [spec.md](../spec.md)
|
| 6 |
+
|
| 7 |
+
## Content Quality
|
| 8 |
+
|
| 9 |
+
- [x] No implementation details (languages, frameworks, APIs)
|
| 10 |
+
- [x] Focused on user value and business needs
|
| 11 |
+
- [x] Written for non-technical stakeholders
|
| 12 |
+
- [x] All mandatory sections completed
|
| 13 |
+
|
| 14 |
+
## Requirement Completeness
|
| 15 |
+
|
| 16 |
+
- [x] No [NEEDS CLARIFICATION] markers remain
|
| 17 |
+
- [x] Requirements are testable and unambiguous
|
| 18 |
+
- [x] Success criteria are measurable
|
| 19 |
+
- [x] Success criteria are technology-agnostic (no implementation details)
|
| 20 |
+
- [x] All acceptance scenarios are defined
|
| 21 |
+
- [x] Edge cases are identified
|
| 22 |
+
- [x] Scope is clearly bounded
|
| 23 |
+
- [x] Dependencies and assumptions identified
|
| 24 |
+
|
| 25 |
+
## Feature Readiness
|
| 26 |
+
|
| 27 |
+
- [x] All functional requirements have clear acceptance criteria
|
| 28 |
+
- [x] User scenarios cover primary flows
|
| 29 |
+
- [x] Feature meets measurable outcomes defined in Success Criteria
|
| 30 |
+
- [x] No implementation details leak into specification
|
| 31 |
+
|
| 32 |
+
## Notes
|
| 33 |
+
|
| 34 |
+
- No clarifications were required: reasonable defaults were assumed for near-duplicate name matching, "unusable" state semantics, and re-binding being out of scope. These are documented in the Assumptions section of spec.md.
|
| 35 |
+
- All checklist items pass on first validation pass.
|
specs/003-knowledge-delete-guardrails/contracts/components.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Component Contracts: Knowledge Source Deletion Guardrails
|
| 2 |
+
|
| 3 |
+
This is a frontend-only feature with no backend/API contract changes. The contracts below describe the new/changed React component prop interfaces so implementation and review have a shared reference.
|
| 4 |
+
|
| 5 |
+
## `src/app/components/ui/alert-dialog.tsx` (new)
|
| 6 |
+
|
| 7 |
+
Thin shadcn-style wrapper around `@radix-ui/react-alert-dialog`, exporting: `AlertDialog`, `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogHeader`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogFooter`, `AlertDialogAction`, `AlertDialogCancel` — same shape as the standard shadcn/ui `alert-dialog` primitive. No project-specific props beyond styling.
|
| 8 |
+
|
| 9 |
+
## `KnowledgeManagement.tsx` — internal confirmation dialog state (no new exported props)
|
| 10 |
+
|
| 11 |
+
New internal state:
|
| 12 |
+
|
| 13 |
+
```ts
|
| 14 |
+
type DeleteIntent =
|
| 15 |
+
| { kind: "document"; docId: string; docName: string }
|
| 16 |
+
| { kind: "database"; clientId: string; clientName: string }
|
| 17 |
+
| { kind: "clear-all-documents" };
|
| 18 |
+
|
| 19 |
+
const [deleteIntent, setDeleteIntent] = useState<DeleteIntent | null>(null);
|
| 20 |
+
const [impact, setImpact] = useState<{ id: string; analysis_title: string }[]>([]);
|
| 21 |
+
const [loadingImpact, setLoadingImpact] = useState(false);
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
Behavior contract:
|
| 25 |
+
- Clicking any delete trigger (single document, single database client, "clear all documents") sets `deleteIntent` instead of calling the delete API directly, then fetches `listAnalyses()` and computes `impact` (see data-model.md `DeletionImpactSummary`).
|
| 26 |
+
- The `AlertDialog` renders open whenever `deleteIntent !== null`.
|
| 27 |
+
- `AlertDialogCancel` → `setDeleteIntent(null); setImpact([])` — no API call.
|
| 28 |
+
- `AlertDialogAction` → performs the existing delete call(s) (`deleteDocument` / `deleteDatabaseClient`, looped for clear-all) that today run directly from the click handler, then closes the dialog.
|
| 29 |
+
- Replaces the existing `window.confirm(...)` call in `deleteAllDocuments` (`KnowledgeManagement.tsx:266`) — that function becomes the `AlertDialogAction` handler for the `clear-all-documents` intent.
|
| 30 |
+
|
| 31 |
+
## `NewAnalysisDialog.tsx`
|
| 32 |
+
|
| 33 |
+
New/changed props:
|
| 34 |
+
|
| 35 |
+
```ts
|
| 36 |
+
interface NewAnalysisDialogProps {
|
| 37 |
+
open: boolean;
|
| 38 |
+
onClose: () => void;
|
| 39 |
+
onCreated: (analysis: Analysis) => void;
|
| 40 |
+
existingAnalyses: Analysis[]; // NEW — passed from AnalysisShell's existing `analyses` state
|
| 41 |
+
}
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
Behavior contract:
|
| 45 |
+
- `canSubmit` gains an additional condition: `!isDuplicateName(title, existingAnalyses)` (see data-model.md).
|
| 46 |
+
- When the title is a duplicate, render an inline error near the title field (same visual pattern as the existing `error` state block) stating the name is already in use; this does not block typing, only submission.
|
| 47 |
+
- Caller (`AnalysisShell.tsx`) passes its existing `analyses` array — no new fetch required.
|
| 48 |
+
|
| 49 |
+
## `SuggestedQuestionsBar.tsx`
|
| 50 |
+
|
| 51 |
+
New prop:
|
| 52 |
+
|
| 53 |
+
```ts
|
| 54 |
+
interface SuggestedQuestionsBarProps {
|
| 55 |
+
questions: string[];
|
| 56 |
+
onSelect: (question: string) => void;
|
| 57 |
+
disabled?: boolean; // NEW
|
| 58 |
+
}
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
Behavior contract: when `disabled` is `true`, every rendered question button gets `disabled` and the existing click handler becomes a no-op (defense in depth beyond the `disabled` HTML attribute).
|
| 62 |
+
|
| 63 |
+
## `ReportSidebar.tsx`
|
| 64 |
+
|
| 65 |
+
New prop:
|
| 66 |
+
|
| 67 |
+
```ts
|
| 68 |
+
interface ReportSidebarProps {
|
| 69 |
+
analysis: Analysis;
|
| 70 |
+
userId?: string;
|
| 71 |
+
onCollapse?: () => void;
|
| 72 |
+
disabled?: boolean; // NEW — true when the analysis is unusable
|
| 73 |
+
}
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
Behavior contract: the generate-report button (`handleGenerate`) is disabled when `disabled` is `true`; report viewing/history (existing report list/version selector) is unaffected and remains fully interactive.
|
| 77 |
+
|
| 78 |
+
## `DataBindSelector.tsx`
|
| 79 |
+
|
| 80 |
+
New prop:
|
| 81 |
+
|
| 82 |
+
```ts
|
| 83 |
+
interface DataBindSelectorProps {
|
| 84 |
+
value: DataBindItem[];
|
| 85 |
+
onChange: (items: DataBindItem[]) => void;
|
| 86 |
+
disabled?: boolean; // meaning extended: also true when hosting analysis is unusable, not just `submitting`
|
| 87 |
+
}
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
Behavior contract: when used inside an unusable analysis's edit/update-data flow (specifically the instance embedded in `AnalysisHeader.tsx`'s source editor), `disabled` is forced `true` regardless of other state (e.g. `saving`), preventing `updateAnalysisDataBind` from being invoked. Per clarification, this applies even though this is the same control a user would otherwise use to fix the stale binding — there is no exception for "trying to recover."
|
| 91 |
+
|
| 92 |
+
## `AnalysisShell.tsx` — orchestration (no new exported props; internal only)
|
| 93 |
+
|
| 94 |
+
New internal derivation, reusing the **already-existing** `staleSources` state (`AnalysisShell.tsx:80`, computed at `AnalysisShell.tsx:117-138`) rather than recomputing it:
|
| 95 |
+
|
| 96 |
+
```ts
|
| 97 |
+
const isActiveAnalysisUnusable = staleSources.length > 0;
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
This value is passed as the new `disabled`/`unusable` prop into `ChatInput`, `HelpSkillButton` (via `ChatInput`'s existing `disabled` passthrough), `SuggestedQuestionsBar`, `ReportSidebar`, and the new `unusable` prop on `AnalysisHeader` (below). FR-010's visible indicator reuses `AnalysisHeader.tsx`'s existing stale-sources banner rather than adding a new one.
|
| 101 |
+
|
| 102 |
+
## `AnalysisHeader.tsx`
|
| 103 |
+
|
| 104 |
+
New prop:
|
| 105 |
+
|
| 106 |
+
```ts
|
| 107 |
+
interface AnalysisHeaderProps {
|
| 108 |
+
analysis: Analysis | null;
|
| 109 |
+
staleSources?: DataBindItem[];
|
| 110 |
+
onUpdateDataBind: (items: DataBindItem[]) => Promise<void>;
|
| 111 |
+
unusable?: boolean; // NEW — equivalent to staleSources.length > 0, passed explicitly for clarity at call sites
|
| 112 |
+
}
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
Behavior contract (per clarification — no in-app recovery path):
|
| 116 |
+
- When `unusable` is `true`, the header's "N sources" button (`openSourceEditor`, ~line 40) is disabled so the editor cannot be opened.
|
| 117 |
+
- The existing stale-sources banner's "Update binding" button (~line 75) is disabled (not just visually — the `onClick` that sets `editingSources(true)` must be prevented).
|
| 118 |
+
- If `editingSources` is somehow already `true` when `unusable` becomes true, the embedded `DataBindSelector` (~line 91) and the "Save sources" button (~line 97) are forced `disabled`.
|
| 119 |
+
- The banner copy may be extended (not replaced) to also convey "this analysis can no longer be used" per FR-010, reusing this single banner for both the pre-existing "stale sources" warning and this feature's "unusable" indicator.
|
specs/003-knowledge-delete-guardrails/data-model.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data Model: Knowledge Source Deletion Guardrails
|
| 2 |
+
|
| 3 |
+
No new persisted entities or backend schema changes. This feature only adds client-side derived values computed from existing `orchestrationApi` types (`src/services/orchestrationApi.ts`).
|
| 4 |
+
|
| 5 |
+
## Existing entities reused (unchanged)
|
| 6 |
+
|
| 7 |
+
- **`ApiDocument`** (`orchestrationApi.ts:60`-ish): `{ id, filename, file_type, file_size, status, created_at, ... }` — a document knowledge source.
|
| 8 |
+
- **`DatabaseClient`**: `{ id, name, db_type, status, ... }` — a database knowledge source.
|
| 9 |
+
- **`DataBindItem`** (`orchestrationApi.ts:150`): `{ id, name, group_type: "document" | "database", type }` — a reference from an `Analysis` to a bound knowledge source.
|
| 10 |
+
- **`Analysis`** (`orchestrationApi.ts:157`): `{ id, analysis_title, objective, business_questions, status: "active" | "inactive" | string, data_bind: DataBindItem[], data_bind_version, ... }`. `status` is **not** modified by this feature (see Clarifications in spec.md).
|
| 11 |
+
|
| 12 |
+
## New client-side derived values (not persisted, not sent to the backend)
|
| 13 |
+
|
| 14 |
+
### `DeletionImpactSummary`
|
| 15 |
+
|
| 16 |
+
Computed at delete-intent time in `KnowledgeManagement.tsx`, before the confirmation dialog opens.
|
| 17 |
+
|
| 18 |
+
| Field | Type | Description |
|
| 19 |
+
|---|---|---|
|
| 20 |
+
| `sourceIds` | `string[]` | The knowledge source id(s) about to be deleted (one for single delete, many for "clear all"). |
|
| 21 |
+
| `affectedAnalyses` | `{ id: string; analysis_title: string }[]` | Analyses whose `data_bind` contains at least one of `sourceIds`, deduplicated by analysis id. Derived by fetching `listAnalyses()` and filtering `analysis.data_bind.some(item => sourceIds.includes(item.id))`. |
|
| 22 |
+
|
| 23 |
+
**Lifecycle**: Computed fresh each time a delete action is initiated; discarded when the dialog closes (confirm or cancel). Never persisted.
|
| 24 |
+
|
| 25 |
+
### Analysis usability flag (`isUnusable`)
|
| 26 |
+
|
| 27 |
+
**Reused, not newly computed**: `AnalysisShell.tsx` already maintains `staleSources: DataBindItem[]` (state at `AnalysisShell.tsx:80`, computed in the `useEffect` at `AnalysisShell.tsx:117-138`) by cross-referencing `activeAnalysis.data_bind` against live `getDocuments`/`getDatabaseClients` results — this is exactly the "unusable" derivation this feature needs, built for feature 002's stale-sources banner. This feature derives from it rather than re-implementing it:
|
| 28 |
+
|
| 29 |
+
```
|
| 30 |
+
isActiveAnalysisUnusable: boolean = staleSources.length > 0
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
- Any single missing bound source is sufficient to mark the whole analysis unusable (per spec Assumptions / Acceptance Scenario US3.6) — already true of `staleSources`'s existing filter logic.
|
| 34 |
+
- This is a pure, stateless derivation — not stored on the `Analysis` object returned by the API, and never written back via `updateAnalysis`/`UpdateAnalysisPayload`.
|
| 35 |
+
- Recomputed on-demand (page load / analysis selection), not via a live subscription (per research.md Unknown 3 and the plan's Constraints) — matching `staleSources`'s existing recompute trigger (the `activeAnalysis`-keyed effect).
|
| 36 |
+
|
| 37 |
+
**State transitions**: `isActiveAnalysisUnusable` has no explicit transition function in this feature — it is a pure function of current data. It can only become `true` when a bound source is deleted (User Story 1 → 3). Per clarification, this feature has **no** transition back to `false`: once `staleSources.length > 0`, the update-bound-data action (including the pre-existing "Update binding" control that would normally clear `staleSources`) is itself disabled by this feature (FR-011/FR-014), so there is no in-app path to restore usability.
|
| 38 |
+
|
| 39 |
+
## Validation rule: analysis name uniqueness
|
| 40 |
+
|
| 41 |
+
Enforced client-side in `NewAnalysisDialog.tsx` before calling `createAnalysis()`:
|
| 42 |
+
|
| 43 |
+
```
|
| 44 |
+
isDuplicateName(candidate: string, existing: Analysis[]): boolean =
|
| 45 |
+
existing.some(a => a.analysis_title.trim().toLowerCase() === candidate.trim().toLowerCase())
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
- Requires the dialog to have access to the current analysis list (already loaded by the parent `AnalysisShell.tsx` as `analyses` state) — passed down as a new prop, or fetched via `listAnalyses()` if the dialog is opened without it in scope.
|
| 49 |
+
- This is a client-side guard only; per FR-007/FR-008 no backend contract change is assumed to exist, so this check does not guarantee uniqueness under concurrent creation from two sessions (acceptable per spec — no server-side uniqueness constraint was requested).
|
specs/003-knowledge-delete-guardrails/plan.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan: Knowledge Source Deletion Guardrails
|
| 2 |
+
|
| 3 |
+
**Branch**: `003-knowledge-delete-guardrails` | **Date**: 2026-07-24 | **Spec**: [spec.md](./spec.md)
|
| 4 |
+
|
| 5 |
+
**Input**: Feature specification from `/specs/003-knowledge-delete-guardrails/spec.md`
|
| 6 |
+
|
| 7 |
+
**Note**: This template is filled in by the `/speckit-plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
|
| 8 |
+
|
| 9 |
+
## Summary
|
| 10 |
+
|
| 11 |
+
Add a confirmation dialog to `KnowledgeManagement.tsx` for every document/database delete action (single delete and "clear all documents") that lists which analyses are bound to the source(s) being removed and warns that those analyses will stop working; add a client-side exact-match (case/whitespace-insensitive) duplicate-name guard to `NewAnalysisDialog.tsx`; and derive a client-side "unusable" state for any analysis whose `data_bind` references a knowledge source that no longer exists, reusing the existing `staleSources` computation already present in `AnalysisShell.tsx`/`AnalysisHeader.tsx` (built for a "some bound sources are no longer available" warning banner) rather than introducing a parallel computation. That derived state is propagated to disable `ChatInput`, `HelpSkillButton`, `SuggestedQuestionsBar`, the report-generation action in `ReportSidebar.tsx`, and — per clarification — the data-bind-update action in `DataBindSelector.tsx` **including the pre-existing "Update binding" recovery control** in `AnalysisHeader.tsx`'s stale-sources banner, so an unusable analysis has no in-app path back to usable; `MessageList.tsx` and existing report viewing remain fully readable. Per clarification, the unusable flag is a standalone derived value — it does **not** reuse or write to the existing `Analysis.status` ("active"/"inactive") field, since that field already drives the "active" filter used to fetch the sidebar analysis list and must not be touched. All changes are frontend-only (React 18 + TypeScript + Tailwind); no new backend endpoints are required — the impact computation and the unusable-state derivation both run client-side against data already available from `listAnalyses()`, `getDocuments()`, and `getDatabaseClients()`.
|
| 12 |
+
|
| 13 |
+
## Technical Context
|
| 14 |
+
|
| 15 |
+
**Language/Version**: TypeScript (React 18.3.1, Vite 6.3.5)
|
| 16 |
+
|
| 17 |
+
**Primary Dependencies**: React 18, Tailwind CSS 4, `@radix-ui/react-alert-dialog` (already installed, unused so far — used for the new confirmation dialog), `lucide-react` icons, `sonner` for toasts
|
| 18 |
+
|
| 19 |
+
**Storage**: N/A — this feature only touches client-side UI state and read-only client-side derivations; `Analysis`/`ApiDocument`/`DatabaseClient` data continues to come from the existing `orchestrationApi` service unchanged, no new persisted fields
|
| 20 |
+
|
| 21 |
+
**Testing**: No test framework is currently configured in this project (no vitest/jest in `devDependencies`); verification is manual/visual, consistent with `specs/002-fix-analysis-chat-ui/plan.md`
|
| 22 |
+
|
| 23 |
+
**Target Platform**: Web browser (desktop primary; existing `KnowledgeManagement` modal/page variants and `AnalysisShell` mobile drawer must keep working)
|
| 24 |
+
|
| 25 |
+
**Project Type**: Single-page web application (Vite + React), frontend-only change — no `backend/` directory exists in this repo
|
| 26 |
+
|
| 27 |
+
**Performance Goals**: Impact-preview computation (matching a source's id against every analysis's `data_bind`) must resolve within 300ms for typical catalogs (dozens of analyses) so the confirmation dialog opens without a visible delay (SC-002)
|
| 28 |
+
|
| 29 |
+
**Constraints**: Unusable state is a standalone derived flag, independent of and never written into `Analysis.status` (per clarification — `status` already filters the sidebar's `listAnalyses({ status: "active" })` call and must not be repurposed); unusable state is recomputed on analysis load/selection (not real-time/live while a user is idle on the page, since no push/websocket mechanism exists in this codebase); deletion remains a soft warning, not a hard block — the confirm action always proceeds once acknowledged (FR-006); once an analysis is flagged unusable it has **no in-app recovery path** — the update-bound-data action is disabled unconditionally, including the pre-existing "Update binding" control in `AnalysisHeader.tsx`'s stale-sources banner (per clarification)
|
| 30 |
+
|
| 31 |
+
**Scale/Scope**: 5 components modified (`KnowledgeManagement.tsx` for the confirmation dialog, `NewAnalysisDialog.tsx` for duplicate-name validation, `AnalysisShell.tsx` for computing/propagating the unusable flag, `ReportSidebar.tsx` and `DataBindSelector.tsx` for disabling report-generation/data-update, `SuggestedQuestionsBar.tsx` for adding a `disabled` prop it currently lacks); no new routes, no new backend endpoints, no new persisted entities
|
| 32 |
+
|
| 33 |
+
## Constitution Check
|
| 34 |
+
|
| 35 |
+
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
| 36 |
+
|
| 37 |
+
The project constitution (`.specify/memory/constitution.md`) is an unfilled template with no ratified principles — there are no constitutional gates to evaluate against. No violations to justify.
|
| 38 |
+
|
| 39 |
+
## Project Structure
|
| 40 |
+
|
| 41 |
+
### Documentation (this feature)
|
| 42 |
+
|
| 43 |
+
```text
|
| 44 |
+
specs/003-knowledge-delete-guardrails/
|
| 45 |
+
├── plan.md # This file (/speckit-plan command output)
|
| 46 |
+
├── research.md # Phase 0 output (/speckit-plan command)
|
| 47 |
+
├── data-model.md # Phase 1 output (/speckit-plan command)
|
| 48 |
+
├── quickstart.md # Phase 1 output (/speckit-plan command)
|
| 49 |
+
├── contracts/ # Phase 1 output (/speckit-plan command) — UI component contracts only
|
| 50 |
+
└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan)
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Source Code (repository root)
|
| 54 |
+
|
| 55 |
+
```text
|
| 56 |
+
src/
|
| 57 |
+
├── app/
|
| 58 |
+
│ └── components/
|
| 59 |
+
│ ├── KnowledgeManagement.tsx # Delete/clear-all handlers — touched for US1 (confirmation dialog + impact preview)
|
| 60 |
+
│ └── analysis/
|
| 61 |
+
│ ├── AnalysisShell.tsx # Derives `isActiveAnalysisUnusable` from existing `staleSources`; wires disabled props — touched for US3
|
| 62 |
+
│ ├── AnalysisHeader.tsx # Existing stale-sources banner + "Update binding" control — touched for US3 (reuse banner for FR-010; disable "Update binding"/"N sources" controls and embedded DataBindSelector when unusable)
|
| 63 |
+
│ ├── NewAnalysisDialog.tsx # Title field validation — touched for US2 (duplicate-name guard)
|
| 64 |
+
│ ├── ChatInput.tsx # Already supports `disabled` — no change, just wired from US3
|
| 65 |
+
│ ├── HelpSkillButton.tsx # Already supports `disabled` — no change, just wired from US3
|
| 66 |
+
│ ├── SuggestedQuestionsBar.tsx # Touched for US3 (add `disabled` prop, currently missing)
|
| 67 |
+
│ ├── ReportSidebar.tsx # `handleGenerate`/generate-report button — touched for US3 (disable when unusable)
|
| 68 |
+
│ ├── DataBindSelector.tsx # Update-bound-data control (already supports `disabled`) — touched for US3 (forced disabled when unusable, both in AnalysisHeader's editor and in the "Update binding" trigger)
|
| 69 |
+
│ └── ui/
|
| 70 |
+
│ └── alert-dialog.tsx # New shadcn/radix-style wrapper around the already-installed
|
| 71 |
+
│ # @radix-ui/react-alert-dialog primitive — reused for US1's confirmation dialog
|
| 72 |
+
└── services/
|
| 73 |
+
└── orchestrationApi.ts # Unchanged; existing Analysis/DataBindItem/ApiDocument/DatabaseClient types reused
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
**Structure Decision**: Single frontend project (`src/app/components/` and `src/app/components/analysis/`) — no new top-level directories. All work happens within the existing Knowledge and Analysis feature folders, reusing the already-installed `@radix-ui/react-alert-dialog` (wrapped once as `src/app/components/ui/alert-dialog.tsx`, matching the existing shadcn-style pattern of `ui/resizable.tsx`) rather than adding a new dependency, and computing the impact list / unusable flag entirely client-side from data already returned by `listAnalyses()`, `getDocuments()`, and `getDatabaseClients()`.
|
| 77 |
+
|
| 78 |
+
## Complexity Tracking
|
| 79 |
+
|
| 80 |
+
> No Constitution Check violations — this section is not applicable.
|
specs/003-knowledge-delete-guardrails/quickstart.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Quickstart: Verifying Knowledge Source Deletion Guardrails
|
| 2 |
+
|
| 3 |
+
Manual verification steps (no test framework configured in this project — see plan.md Testing).
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
- Run the app: `pnpm dev` (or `npm run dev` per `package.json` scripts) and sign in with a user that has at least:
|
| 8 |
+
- One document knowledge source bound to at least one analysis
|
| 9 |
+
- One database knowledge source not bound to any analysis
|
| 10 |
+
- Two existing analyses with distinct names
|
| 11 |
+
|
| 12 |
+
## User Story 1 — Deletion confirmation with impact preview
|
| 13 |
+
|
| 14 |
+
1. Open the Knowledge menu, find a document bound to an analysis, click its delete icon.
|
| 15 |
+
2. Verify a confirmation dialog opens (not a browser `confirm()` popup) listing the bound analysis by name and a warning that it will become unusable.
|
| 16 |
+
3. Click Cancel — verify the document is still listed and no analysis state changed.
|
| 17 |
+
4. Click delete again, click Confirm — verify the document disappears from the list.
|
| 18 |
+
5. Delete a database source with **no** bound analyses — verify the dialog still appears but states no analyses will be affected.
|
| 19 |
+
6. Click "Clear all" on documents with multiple docs (some bound to analyses) — verify the dialog lists the combined, deduplicated set of affected analyses.
|
| 20 |
+
|
| 21 |
+
## User Story 2 — Duplicate analysis name guard
|
| 22 |
+
|
| 23 |
+
1. Open "New Analysis", enter a title that exactly matches an existing analysis's title (try exact, then with different case/leading spaces).
|
| 24 |
+
2. Verify submission is blocked and an inline message indicates the name is already in use.
|
| 25 |
+
3. Change the title to something unique — verify the create button becomes usable and creation succeeds.
|
| 26 |
+
4. Repeat step 1 with a different set of bound knowledge sources selected — verify the duplicate-name check still blocks submission the same way (FR-008).
|
| 27 |
+
|
| 28 |
+
## User Story 3 — Unusable analysis after source deletion
|
| 29 |
+
|
| 30 |
+
1. From User Story 1 step 4, open the analysis that was bound to the now-deleted document.
|
| 31 |
+
2. Verify the existing "some bound sources are no longer available" banner is shown and now also states the analysis can no longer be used (this feature extends that banner rather than adding a new one).
|
| 32 |
+
3. Verify: chat input textarea, Send button, Help button, and every suggested/recommended question button are disabled.
|
| 33 |
+
4. Verify: the "Generate report" action is disabled.
|
| 34 |
+
5. Verify: the "N sources" button and the banner's "Update binding" button are both disabled — clicking them does nothing, and the bound-data editor cannot be opened at all. This is intentional: per product decision, an unusable analysis has no in-app way to recover by re-binding a replacement source.
|
| 35 |
+
6. Verify: existing chat messages are still visible/scrollable, and any previously generated report can still be opened and read.
|
| 36 |
+
7. Try pressing Enter in the (disabled) chat textarea — verify no message is sent.
|
| 37 |
+
|
| 38 |
+
## Regression checks
|
| 39 |
+
|
| 40 |
+
- An analysis with no bound sources at all still behaves exactly as before (not flagged unusable).
|
| 41 |
+
- An analysis with multiple bound sources where only one was deleted is still flagged unusable (any single missing source is sufficient).
|
| 42 |
+
- The Analysis sidebar list (`listAnalyses({ status: "active" })`) still shows the unusable analysis — confirming `status` was not repurposed/changed by this feature.
|
specs/003-knowledge-delete-guardrails/research.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Research: Knowledge Source Deletion Guardrails
|
| 2 |
+
|
| 3 |
+
## Unknown 1: How to compute "which analyses are affected" by deleting a source
|
| 4 |
+
|
| 5 |
+
**Decision**: On delete-intent (before showing the confirmation dialog), call `listAnalyses()` (existing `orchestrationApi` function, already used in `AnalysisShell.tsx`) to get the current analysis set, then filter client-side for analyses whose `data_bind: DataBindItem[]` array contains an item whose `id` matches the source(s) being deleted.
|
| 6 |
+
|
| 7 |
+
**Rationale**: There is no dedicated "which analyses use source X" backend endpoint. `Analysis.data_bind` (`src/services/orchestrationApi.ts:164`) already embeds the bound source refs (`{ id, name, group_type, type }`) per analysis, so the join is cheap to do client-side and requires no new API contract. `KnowledgeManagement.tsx` does not currently import `listAnalyses`, so this is a new cross-feature dependency (Knowledge → Analysis service) but stays within the existing `orchestrationApi` module — no new service needed.
|
| 8 |
+
|
| 9 |
+
**Alternatives considered**:
|
| 10 |
+
- New backend endpoint returning source→analyses reverse index: rejected as unnecessary backend work for a computation the client can already do from data it fetches anyway (dozens of analyses is trivial to filter in-browser, matching the plan's <300ms budget).
|
| 11 |
+
- Precomputing/caching the reverse index in a global store: rejected as premature — `listAnalyses()` is called fresh at KnowledgeManagement delete-intent time; no evidence of a global state library (Redux/Zustand) in this codebase to justify one.
|
| 12 |
+
|
| 13 |
+
## Unknown 2: Confirmation dialog component to use
|
| 14 |
+
|
| 15 |
+
**Decision**: Use `@radix-ui/react-alert-dialog` (already in `package.json` dependencies, currently unused anywhere in `src/`), wrapped once as `src/app/components/ui/alert-dialog.tsx` in the same shadcn-style pattern as the existing `src/app/components/ui/resizable.tsx` wrapper (per `specs/002-fix-analysis-chat-ui/plan.md` precedent of reusing already-installed-but-unused Radix primitives instead of adding new dependencies).
|
| 16 |
+
|
| 17 |
+
**Rationale**: `AlertDialog` (vs. plain `Dialog`) is the Radix-recommended primitive specifically for "interrupt the user with imperative content and expect a response" confirmation flows — matches this feature's cancel/confirm requirement (FR-005/FR-006) and provides built-in focus-trap/escape/backdrop behavior the current `window.confirm` in `deleteAllDocuments` (`KnowledgeManagement.tsx:266`) lacks styling control over.
|
| 18 |
+
|
| 19 |
+
**Alternatives considered**:
|
| 20 |
+
- Keep using `window.confirm`: rejected — cannot render a formatted, scrollable list of affected analysis names (FR-002), which the current single-string browser confirm cannot do.
|
| 21 |
+
- `@radix-ui/react-dialog` (already used elsewhere for `NewAnalysisDialog`'s manual overlay, though that component hand-rolls its own overlay rather than importing the Radix dialog primitive): rejected in favor of `AlertDialog` for the confirmation-specific semantics and to keep a single reusable primitive for this exact "are you sure" pattern.
|
| 22 |
+
|
| 23 |
+
## Unknown 3: Where "unusable" is computed and how it propagates
|
| 24 |
+
|
| 25 |
+
**Decision**: **Reuse the existing `staleSources` state already implemented in `AnalysisShell.tsx:80,117-138`** (added for feature 002's stale-sources banner) rather than computing a new/parallel derivation. `staleSources` already cross-references `activeAnalysis.data_bind` against live `getDocuments`/`getDatabaseClients` results and is recomputed in a `useEffect` keyed on `activeAnalysis`. Derive `isActiveAnalysisUnusable = staleSources.length > 0` from it. Thread that boolean as a `disabled`/`unusable` prop into `ChatInput`, `HelpSkillButton` (both already accept `disabled`), a new `disabled` prop added to `SuggestedQuestionsBar`, and new disable conditions added to `ReportSidebar`'s generate-report button, `DataBindSelector`'s update action, and — per the second clarification — `AnalysisHeader.tsx`'s existing "Update binding" button and "N sources" trigger (both of which currently open the same `DataBindSelector`-backed editor used to fix `staleSources`).
|
| 26 |
+
|
| 27 |
+
**Rationale**: `AnalysisShell.tsx` already computes exactly the value this feature needs — an initial pass at this research (before source inspection) proposed a new computation, but that would have duplicated `staleSources` and diverged from it over time (see analysis finding A1). Per the first clarification answer, the resulting flag must remain standalone from `Analysis.status` — reusing `status` would corrupt the existing `listAnalyses({ status: "active" })` sidebar filter (`AnalysisShell.tsx:101`), silently hiding "unusable" analyses from the sidebar list, which directly contradicts FR-012 (chat history/reports must remain viewable/reachable). Per the second clarification answer, the pre-existing "Update binding" recovery control must **also** be disabled once `isActiveAnalysisUnusable` is true, even though that control already exists and today lets users fix exactly this condition — the feature intentionally removes that recovery path rather than special-casing it, so an unusable analysis has no in-app way back to usable (FR-009).
|
| 28 |
+
|
| 29 |
+
**Alternatives considered**:
|
| 30 |
+
- Real-time/live re-evaluation while the analysis screen is open (e.g., polling or a websocket) to catch a source being deleted in another tab while the user is mid-session: rejected as out of scope per the plan's Constraints — no push mechanism exists in this codebase, and the spec's Edge Cases only require "fail gracefully" on stale-state actions, not live UI updates.
|
| 31 |
+
- Storing the unusable flag as a new persisted field on `Analysis` returned by the backend: rejected — no backend contract change is needed since the flag is fully derivable client-side from data already fetched, keeping this a frontend-only change per the Summary.
|
| 32 |
+
- Keeping the "Update binding" control enabled as a recovery escape hatch while disabling everything else: considered and rejected per clarification — the user explicitly chose "disable it too" over preserving recovery.
|
specs/003-knowledge-delete-guardrails/spec.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Feature Specification: Knowledge Source Deletion Guardrails
|
| 2 |
+
|
| 3 |
+
**Feature Branch**: `003-knowledge-delete-guardrails`
|
| 4 |
+
|
| 5 |
+
**Created**: 2026-07-24
|
| 6 |
+
|
| 7 |
+
**Status**: Draft
|
| 8 |
+
|
| 9 |
+
**Input**: User description: "ada beberapa behavior frontend yang perlu diperbaiki, 1. di menu knowledge, ketika user hendak menghapus salah satu atau semua sources (baik database / document) maka perlu ada pop up konfirmasi lagi ke user, apakah user benar-benar setuju atau tidak. Pada pop up tersebut ada informasi yang menjelaskan impactnya jika source tersebut dihapus (contoh: dokumen A atau database X digunakan pada Analisis XXX, analisis XXX tidak akan bisa digunakan lagi). Kemudian perlu ada rules untuk nama analysis yang di create tidak boleh ada yang sama persis. 2. Apabila user yakin untuk menghapus knowledge source (yang sudah di bind ke analysis), maka user tidak akan bisa lagi mengirim pesan ke analysis ini, tidak bisa update data yang ter-bind ke analysis ini, kemudian ada flagging analysis ini tidak bisa digunakan lagi meskipun masih bisa melihat chat history atau melihat report yang pernah ter-generate sebelumnya. Tombol tulis chat, kirim chat, generate report, help, recommendation question jadi disable karena ada knowledge yang terhapus."
|
| 10 |
+
|
| 11 |
+
## Clarifications
|
| 12 |
+
|
| 13 |
+
### Session 2026-07-24
|
| 14 |
+
|
| 15 |
+
- Q: Should the "unusable" state reuse the existing analysis status concept, or be a separate independent flag? → A: "Unusable" is a separate, independent flag computed purely from comparing bound sources vs. existing sources, unrelated to any status field
|
| 16 |
+
- Q: The codebase already has a "stale sources" banner (`AnalysisHeader.tsx`) with an "Update binding" control that lets a user re-bind a replacement source. Should that control stay enabled for an unusable analysis so the user can recover it, or be disabled along with everything else? → A: Disabled along with everything else — an unusable analysis has no in-app recovery path; the flag is permanent for that analysis within this feature's scope.
|
| 17 |
+
|
| 18 |
+
## User Scenarios & Testing *(mandatory)*
|
| 19 |
+
|
| 20 |
+
### User Story 1 - Confirm knowledge source deletion with impact preview (Priority: P1)
|
| 21 |
+
|
| 22 |
+
A user managing Knowledge sources (documents or database connections) clicks delete on a single source, or clicks "clear all" to delete every source. Before anything is actually removed, the system shows a confirmation dialog that lists the impact of the deletion — specifically, which analyses currently use that source and a warning that those analyses will stop working. The user must explicitly confirm before the deletion proceeds; they can also cancel and keep the source.
|
| 23 |
+
|
| 24 |
+
**Why this priority**: Deleting a knowledge source is destructive and currently happens with no confirmation at all (or only a generic browser confirm for "clear all"), risking accidental data loss and silently broken analyses. This is the foundational safeguard the rest of the feature depends on.
|
| 25 |
+
|
| 26 |
+
**Independent Test**: Can be fully tested by attempting to delete a document or database source (used by at least one analysis) and verifying a confirmation dialog appears listing the affected analyses by name, and that choosing "cancel" leaves the source and all analyses untouched, while choosing "confirm" proceeds with deletion.
|
| 27 |
+
|
| 28 |
+
**Acceptance Scenarios**:
|
| 29 |
+
|
| 30 |
+
1. **Given** a document source that is bound to one or more analyses, **When** the user clicks delete on that source, **Then** a confirmation dialog appears listing each affected analysis by name and stating those analyses will no longer be usable after deletion.
|
| 31 |
+
2. **Given** a database source that is not bound to any analysis, **When** the user clicks delete on that source, **Then** a confirmation dialog appears indicating no analyses will be affected, and the user can still confirm or cancel.
|
| 32 |
+
3. **Given** the confirmation dialog is open, **When** the user clicks "cancel", **Then** the source is not deleted and the dialog closes.
|
| 33 |
+
4. **Given** the confirmation dialog is open, **When** the user clicks "confirm", **Then** the source is deleted and any bound analyses are flagged as unusable (see User Story 3).
|
| 34 |
+
5. **Given** the user clicks "clear all sources", **When** the confirmation dialog appears, **Then** it lists the combined set of all analyses that would be affected across all sources being cleared.
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
### User Story 2 - Prevent duplicate analysis names (Priority: P2)
|
| 39 |
+
|
| 40 |
+
A user creating a new analysis enters a name that is identical to the name of an existing analysis. The system blocks creation and informs the user that the name is already in use, prompting them to choose a different name.
|
| 41 |
+
|
| 42 |
+
**Why this priority**: Duplicate analysis names create ambiguity when browsing, searching, or referencing analyses (including in the impact-preview list from User Story 1), so this is important but independent of the deletion-confirmation flow and can ship separately.
|
| 43 |
+
|
| 44 |
+
**Independent Test**: Can be fully tested by creating an analysis with a name, then attempting to create a second analysis using the exact same name, and verifying the second attempt is blocked with a clear message while a differently-named analysis creates successfully.
|
| 45 |
+
|
| 46 |
+
**Acceptance Scenarios**:
|
| 47 |
+
|
| 48 |
+
1. **Given** an existing analysis named "Sales Q1", **When** a user attempts to create a new analysis also named "Sales Q1" (exact match), **Then** the system prevents submission and shows a message that the name is already in use.
|
| 49 |
+
2. **Given** an existing analysis named "Sales Q1", **When** a user creates a new analysis named "sales q1 " (different casing/whitespace) or "Sales Q2", **Then** the near-duplicate is blocked per the matching rule in Assumptions, and the non-matching name is allowed to proceed.
|
| 50 |
+
3. **Given** the name-in-use message is shown, **When** the user edits the name to something unique, **Then** the creation action becomes available again.
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
### User Story 3 - Disable a usable analysis after its knowledge source is deleted (Priority: P1)
|
| 55 |
+
|
| 56 |
+
A user opens an analysis whose bound knowledge source(s) have been deleted (via User Story 1). The analysis is visibly flagged as no longer usable. The user can still browse the full chat history and view previously generated reports, but every action that would create new activity — writing/sending a chat message, using the help/skill button, using suggested/recommended questions, generating a new report, or updating the analysis's bound data — is disabled.
|
| 57 |
+
|
| 58 |
+
**Why this priority**: Without this, a deleted source leaves an analysis in a broken state where users can still attempt actions that will fail unexpectedly (e.g., sending a chat message with no backing data), producing confusing errors instead of a clear, upfront explanation. This is the direct consequence of User Story 1 and is equally critical.
|
| 59 |
+
|
| 60 |
+
**Independent Test**: Can be fully tested by deleting a knowledge source bound to an existing analysis, then opening that analysis and verifying: a visible "unusable" flag/banner is shown, chat input/send/help/suggested-questions/generate-report/update-data controls are disabled, while chat history and prior reports remain viewable.
|
| 61 |
+
|
| 62 |
+
**Acceptance Scenarios**:
|
| 63 |
+
|
| 64 |
+
1. **Given** an analysis bound only to a knowledge source that has since been deleted, **When** the user opens that analysis, **Then** a clear indicator (e.g., banner or label) explains the analysis is unusable because a bound knowledge source was deleted.
|
| 65 |
+
2. **Given** an analysis flagged as unusable, **When** the user views the chat panel, **Then** the chat input field, send button, help button, and suggested/recommended question buttons are all disabled.
|
| 66 |
+
3. **Given** an analysis flagged as unusable, **When** the user looks for a way to generate a new report, **Then** the generate-report action is disabled.
|
| 67 |
+
4. **Given** an analysis flagged as unusable, **When** the user looks for a way to change its bound data sources, **Then** the update/change-bound-data action is disabled.
|
| 68 |
+
5. **Given** an analysis flagged as unusable, **When** the user scrolls through existing chat messages or opens a previously generated report, **Then** the content is fully viewable exactly as before.
|
| 69 |
+
6. **Given** an analysis is bound to multiple knowledge sources and only one is deleted, **When** the user opens the analysis, **Then** the analysis is still flagged unusable (any missing bound source is sufficient to disable it) per the rule in Assumptions.
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
### Edge Cases
|
| 74 |
+
|
| 75 |
+
- What happens if a user has the deletion confirmation dialog open and, in another tab/session, the source is already deleted or an affected analysis is deleted first? The confirmation action should fail gracefully with a message rather than silently succeeding or erroring uninformatively.
|
| 76 |
+
- What happens when a knowledge source that was deleted is later replaced or a new source with a similar name is added? The previously flagged analysis remains unusable — flagging is tied to the specific deleted source/binding, not resolved by adding an unrelated new source, unless the user explicitly re-binds the analysis to a valid source (out of scope for this feature; see Assumptions).
|
| 77 |
+
- How does the impact list behave when a source is bound to a very large number of analyses? The dialog should present the list in a readable, scannable way (e.g., scrollable list) rather than breaking the layout.
|
| 78 |
+
- What happens if the user attempts analysis creation with a name that only differs by leading/trailing whitespace or case from an existing name? Covered by the near-duplicate rule in Assumptions.
|
| 79 |
+
- What happens to an analysis that has zero bound sources at all (never bound, not a deletion scenario)? This feature only concerns analyses that had a bound source removed; analyses with no bound source retain their current existing behavior.
|
| 80 |
+
|
| 81 |
+
## Requirements *(mandatory)*
|
| 82 |
+
|
| 83 |
+
### Functional Requirements
|
| 84 |
+
|
| 85 |
+
- **FR-001**: System MUST show a confirmation dialog before deleting any single knowledge source (document or database), and before executing a "clear all sources" action.
|
| 86 |
+
- **FR-002**: The confirmation dialog MUST display, for the source(s) being deleted, the list of analyses currently bound to it/them by name, so the user understands which analyses will stop working.
|
| 87 |
+
- **FR-003**: The confirmation dialog MUST clearly state that listed analyses will no longer be usable if the deletion proceeds.
|
| 88 |
+
- **FR-004**: If a source being deleted is not bound to any analysis, the confirmation dialog MUST still appear but indicate that no analyses will be affected.
|
| 89 |
+
- **FR-005**: The user MUST be able to cancel the confirmation dialog, in which case no deletion occurs and no analysis state changes.
|
| 90 |
+
- **FR-006**: The user MUST be able to confirm the dialog, in which case the source is deleted and any bound analyses are flagged as unusable per FR-009.
|
| 91 |
+
- **FR-007**: System MUST prevent creating a new analysis whose name is an exact (case/whitespace-insensitive) duplicate of an existing analysis's name, and MUST inform the user of the conflict.
|
| 92 |
+
- **FR-008**: System MUST apply the duplicate-name check consistently regardless of which knowledge sources are bound to the new analysis.
|
| 93 |
+
- **FR-009**: When a knowledge source is deleted, System MUST flag every analysis bound to that source as unusable. This flag is permanent for that analysis within this feature's scope: no in-app action can restore usability, because the update-bound-data action is itself disabled once an analysis is unusable (see FR-011/FR-014), including any pre-existing "update binding" control (e.g., the stale-sources banner's recovery button). Restoring usability would require a separate, out-of-scope capability.
|
| 94 |
+
- **FR-010**: An analysis flagged as unusable MUST visibly communicate to the user why it is unusable (i.e., that a bound knowledge source was deleted). This indicator MAY reuse/extend an existing "sources are no longer available" banner if one already exists in the UI, rather than requiring a separate new banner.
|
| 95 |
+
- **FR-011**: For an analysis flagged as unusable, System MUST disable: the chat input field, the send-message action, the help/skill action, the suggested/recommended question actions, the generate-report action, and the update/change-bound-data action — including any pre-existing control whose purpose is to let the user fix/re-bind the analysis's data sources (e.g., an existing "update binding" recovery button).
|
| 96 |
+
- **FR-012**: For an analysis flagged as unusable, System MUST continue to allow full read access to existing chat history and previously generated reports.
|
| 97 |
+
- **FR-013**: System MUST prevent a chat message from being sent to an unusable analysis even if attempted through means other than the disabled button (e.g., keyboard submit).
|
| 98 |
+
- **FR-014**: System MUST prevent updating the bound data of an unusable analysis even if attempted through means other than the disabled control.
|
| 99 |
+
|
| 100 |
+
### Key Entities *(include if feature involves data)*
|
| 101 |
+
|
| 102 |
+
- **Knowledge Source**: A document or database connection available for use by analyses; has an identifier, a type (document/database), a name, and zero or more analyses bound to it.
|
| 103 |
+
- **Analysis**: A chat-based analysis bound to one or more knowledge sources; has a name (must be unique among analyses), chat history, generated reports, and a usability state (usable / unusable). This usability state is a standalone flag derived solely from comparing bound source references against currently existing knowledge sources — it is independent of and does not overload any other existing analysis status/lifecycle field.
|
| 104 |
+
- **Deletion Impact Summary**: The set of analyses affected by deleting one or more knowledge sources, computed and shown at confirmation time.
|
| 105 |
+
|
| 106 |
+
## Success Criteria *(mandatory)*
|
| 107 |
+
|
| 108 |
+
### Measurable Outcomes
|
| 109 |
+
|
| 110 |
+
- **SC-001**: 100% of knowledge source deletions (single or "clear all") require explicit user confirmation before any data is removed.
|
| 111 |
+
- **SC-002**: Users can identify, before confirming a deletion, which specific analyses will be affected without leaving the confirmation dialog or performing any additional clicks/scrolling beyond what's needed to read a short list.
|
| 112 |
+
- **SC-003**: Zero analyses can be created with a name that exactly (case/whitespace-insensitive) duplicates an existing analysis name.
|
| 113 |
+
- **SC-004**: 100% of analyses whose bound knowledge source was deleted show a visible unusable indicator and have chat, help, suggested questions, generate report, and data-update actions disabled on next view.
|
| 114 |
+
- **SC-005**: Users can still access 100% of previously generated chat history and reports for an unusable analysis, with zero loss of read access.
|
| 115 |
+
|
| 116 |
+
## Assumptions
|
| 117 |
+
|
| 118 |
+
- Near-duplicate analysis names (differing only by case or leading/trailing whitespace) are treated as duplicates and blocked; matching is case-insensitive and ignores leading/trailing whitespace. Names that differ by internal spacing, punctuation, or any other characters are treated as distinct.
|
| 119 |
+
- "Unusable" is a client-visible state derived from comparing an analysis's bound source references against the current set of existing knowledge sources (i.e., any bound source missing marks the analysis unusable); a single missing source is enough to disable the whole analysis, even if other bound sources are still intact.
|
| 120 |
+
- Re-binding an unusable analysis to a new/replacement knowledge source to restore usability is explicitly disabled by this feature (per clarification), even where a pre-existing UI control for updating bound data already exists; building an alternate/separate recovery capability is out of scope for this feature.
|
| 121 |
+
- The impact-preview list in the confirmation dialog shows analysis names only (not full analysis details); clicking into individual analyses from the dialog is not required.
|
| 122 |
+
- "Clear all sources" refers to deleting all knowledge sources of the type(s) currently in scope of that action (e.g., all documents, or all sources), consistent with existing clear-all behavior in the Knowledge menu.
|
| 123 |
+
- Existing analyses that currently have no bound knowledge source at all are unaffected by this feature and keep their current behavior.
|
| 124 |
+
- Deletion of a knowledge source is still permitted even when it would make analyses unusable; the confirmation dialog is a warning, not a hard block — the user retains final say once informed of the impact.
|
specs/003-knowledge-delete-guardrails/tasks.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
|
| 3 |
+
description: "Task list for Knowledge Source Deletion Guardrails"
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Tasks: Knowledge Source Deletion Guardrails
|
| 8 |
+
|
| 9 |
+
**Input**: Design documents from `/specs/003-knowledge-delete-guardrails/`
|
| 10 |
+
|
| 11 |
+
**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md), [data-model.md](./data-model.md), [contracts/components.md](./contracts/components.md), [quickstart.md](./quickstart.md)
|
| 12 |
+
|
| 13 |
+
**Tests**: Not requested in the feature specification — no automated test framework is configured in this project (see plan.md Technical Context). Verification is manual, per `quickstart.md`.
|
| 14 |
+
|
| 15 |
+
**Organization**: Tasks are grouped by user story (US1–US3, matching spec.md priorities P1/P2/P1) to enable independent implementation and testing of each story.
|
| 16 |
+
|
| 17 |
+
## Format: `[ID] [P?] [Story] Description`
|
| 18 |
+
|
| 19 |
+
- **[P]**: Can run in parallel (different files, no dependencies)
|
| 20 |
+
- **[Story]**: Which user story this task belongs to
|
| 21 |
+
- Tasks touching `src/app/components/analysis/AnalysisShell.tsx` are never marked `[P]` relative to each other — they share one file and must be applied sequentially in ID order.
|
| 22 |
+
|
| 23 |
+
## Path Conventions
|
| 24 |
+
|
| 25 |
+
Single frontend project. All paths are relative to repo root.
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## Phase 1: Setup
|
| 30 |
+
|
| 31 |
+
**Purpose**: Add the one shared, reusable primitive this feature needs before any user story can use it.
|
| 32 |
+
|
| 33 |
+
- [X] T001 [P] Create `src/app/components/ui/alert-dialog.tsx`, a shadcn-style wrapper around the already-installed `@radix-ui/react-alert-dialog` dependency, exporting `AlertDialog`, `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogHeader`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogFooter`, `AlertDialogAction`, `AlertDialogCancel` (per contracts/components.md), styled consistently with existing `src/app/components/ui/*` primitives (e.g. `resizable.tsx`). No new dependency to install — `@radix-ui/react-alert-dialog` is already in `package.json`. — **Already existed in the codebase** with all required exports; no work needed, confirmed by reading the file.
|
| 34 |
+
|
| 35 |
+
**Checkpoint**: `AlertDialog` primitive available for import; ready for US1.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## Phase 2: Foundational (Blocking Prerequisites)
|
| 40 |
+
|
| 41 |
+
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented.
|
| 42 |
+
|
| 43 |
+
**N/A for this feature** — US1 (Knowledge deletion confirmation), US2 (duplicate analysis name), and US3 (disable unusable analysis) touch disjoint sets of components (`KnowledgeManagement.tsx` vs. `NewAnalysisDialog.tsx` vs. `AnalysisShell.tsx`/`ReportSidebar.tsx`/`DataBindSelector.tsx`/`SuggestedQuestionsBar.tsx`) and share no blocking data model, auth, or routing scaffolding. Each story is independently implementable once Phase 1's `AlertDialog` primitive exists (only needed by US1).
|
| 44 |
+
|
| 45 |
+
**Checkpoint**: Proceed directly to user story implementation.
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
## Phase 3: User Story 1 - Confirm knowledge source deletion with impact preview (Priority: P1) 🎯 MVP
|
| 50 |
+
|
| 51 |
+
**Goal**: Deleting a document or database source (single, or "clear all documents") always shows a confirmation dialog listing which analyses will be affected before anything is actually removed.
|
| 52 |
+
|
| 53 |
+
**Independent Test**: Delete a document/database source bound to at least one analysis, verify the dialog lists the affected analysis by name and that Cancel leaves everything untouched while Confirm proceeds with deletion (see quickstart.md § User Story 1).
|
| 54 |
+
|
| 55 |
+
### Implementation for User Story 1
|
| 56 |
+
|
| 57 |
+
- [X] T002 [US1] In `src/app/components/KnowledgeManagement.tsx`, import `listAnalyses` from `../../services/orchestrationApi` and add internal state: `deleteIntent: DeleteIntent | null`, `impact: { id: string; analysis_title: string }[]`, `loadingImpact: boolean` (per contracts/components.md `DeleteIntent` union: `document`, `database`, `clear-all-documents`).
|
| 58 |
+
- [X] T003 [US1] In `src/app/components/KnowledgeManagement.tsx`, add a helper `computeImpact(sourceIds: string[])` that calls `listAnalyses()` and filters/dedupes analyses whose `data_bind` contains any of `sourceIds` (per data-model.md `DeletionImpactSummary`), setting `loadingImpact`/`impact` around the call.
|
| 59 |
+
- [X] T004 [US1] In `src/app/components/KnowledgeManagement.tsx`, change the document delete button's `onClick` (currently calling `handleDeleteDocument(doc.id)` directly, ~line 687) to instead call `setDeleteIntent({ kind: "document", docId: doc.id, docName: doc.filename })` and `computeImpact([doc.id])`. Depends on T002, T003.
|
| 60 |
+
- [X] T005 [P] [US1] In `src/app/components/KnowledgeManagement.tsx`, change the database client delete button's `onClick` (currently calling `handleDeleteClient(client.id)` directly, ~line 617) to instead call `setDeleteIntent({ kind: "database", clientId: client.id, clientName: client.name })` and `computeImpact([client.id])`. Depends on T002, T003.
|
| 61 |
+
- [X] T006 [P] [US1] In `src/app/components/KnowledgeManagement.tsx`, replace the `window.confirm(...)` call in `deleteAllDocuments` (~line 266) with `setDeleteIntent({ kind: "clear-all-documents" })` and `computeImpact(documents.map(d => d.id))`. Depends on T002, T003.
|
| 62 |
+
- [X] T007 [US1] In `src/app/components/KnowledgeManagement.tsx`, render an `AlertDialog` (imported from `./ui/alert-dialog`, added in T001) open whenever `deleteIntent !== null`, with: a title/description branching on `deleteIntent.kind` (per FR-002/FR-003), a scrollable list of `impact` analysis titles when `impact.length > 0` (per Edge Cases: large impact lists must stay scannable) or a "no analyses will be affected" message when `impact.length === 0` (FR-004), an `AlertDialogCancel` that resets `deleteIntent`/`impact` with no API call (FR-005), and an `AlertDialogAction` that performs the actual delete for the given `deleteIntent.kind` — calling existing `handleDeleteDocument`/`handleDeleteClient` for single deletes, or looping `deleteDocument` calls (the existing body of `deleteAllDocuments` minus its `window.confirm`) for `clear-all-documents` — then resets `deleteIntent`/`impact` (FR-006). Depends on T004, T005, T006.
|
| 63 |
+
- [ ] T008 [US1] Manually verify per quickstart.md § User Story 1 — **NOT YET DONE, requires a human/browser pass**; `npm run build` succeeds with no compile/runtime-startup errors (confirms code correctness), but actual UI/UX verification needs manual browser testing: confirm dialog appears for a bound and an unbound source, for clear-all with mixed bound/unbound docs, and that Cancel vs. Confirm behave per FR-005/FR-006. Depends on T007.
|
| 64 |
+
|
| 65 |
+
**Checkpoint**: Knowledge deletion confirmation with impact preview fully functional and independently testable per quickstart.md § User Story 1.
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
## Phase 4: User Story 2 - Prevent duplicate analysis names (Priority: P2)
|
| 70 |
+
|
| 71 |
+
**Goal**: Creating a new analysis with a name that exactly (case/whitespace-insensitively) matches an existing analysis's name is blocked with a clear message.
|
| 72 |
+
|
| 73 |
+
**Independent Test**: Create an analysis, then attempt to create a second one with the identical (or same-but-cased/whitespace-padded) name, and verify it's blocked while a distinct name succeeds (see quickstart.md § User Story 2).
|
| 74 |
+
|
| 75 |
+
### Implementation for User Story 2
|
| 76 |
+
|
| 77 |
+
- [X] T009 [P] [US2] In `src/app/components/analysis/NewAnalysisDialog.tsx`, add a new required prop `existingAnalyses: Analysis[]` to `NewAnalysisDialogProps` (per contracts/components.md) and a helper `isDuplicateName(candidate: string, existing: Analysis[])` comparing `analysis_title.trim().toLowerCase()` (per data-model.md).
|
| 78 |
+
- [X] T010 [US2] In `src/app/components/analysis/NewAnalysisDialog.tsx`, add `isDuplicateName(title, existingAnalyses)` as an additional `canSubmit` condition, and render an inline error near the title field (same visual pattern as the existing `error` block) when the current title is a duplicate, without blocking further typing. Depends on T009.
|
| 79 |
+
- [X] T011 [US2] In `src/app/components/analysis/AnalysisShell.tsx`, pass `existingAnalyses={analyses}` (the already-loaded sidebar analyses state) to `<NewAnalysisDialog />` (~line 554).
|
| 80 |
+
- [ ] T012 [US2] Manually verify per quickstart.md § User Story 2 — **NOT YET DONE, requires a human/browser pass**: exact match blocked, case/whitespace variant blocked, distinct name succeeds, editing to a unique name re-enables submission, and the check behaves the same regardless of which knowledge sources are bound to the new analysis (FR-008). Depends on T010, T011.
|
| 81 |
+
|
| 82 |
+
**Checkpoint**: Duplicate analysis name guard fully functional and independently testable per quickstart.md § User Story 2.
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
## Phase 5: User Story 3 - Disable a usable analysis after its knowledge source is deleted (Priority: P1)
|
| 87 |
+
|
| 88 |
+
**Goal**: An analysis whose bound knowledge source no longer exists is visibly flagged unusable and has all activity-generating actions (chat, help, suggested questions, generate report, update bound data) disabled, while chat history and reports remain fully viewable.
|
| 89 |
+
|
| 90 |
+
**Independent Test**: Delete a knowledge source bound to an existing analysis (via the app's existing delete action), open that analysis, and verify the unusable banner appears, all listed actions are disabled, and history/reports remain viewable (see quickstart.md § User Story 3).
|
| 91 |
+
|
| 92 |
+
### Implementation for User Story 3
|
| 93 |
+
|
| 94 |
+
- [X] T013 [P] [US3] In `src/app/components/analysis/SuggestedQuestionsBar.tsx`, add an optional `disabled?: boolean` prop; when `true`, render every question button with the `disabled` attribute and make the existing `onSelect` handler a no-op (per contracts/components.md).
|
| 95 |
+
- [X] T014 [P] [US3] In `src/app/components/analysis/ReportSidebar.tsx`, add an optional `disabled?: boolean` prop; when `true`, disable the generate-report button (`handleGenerate`'s trigger) while leaving the report list/version selector/report viewing fully interactive (per contracts/components.md, FR-011/FR-012).
|
| 96 |
+
- [X] T015 [P] [US3] In `src/app/components/analysis/AnalysisHeader.tsx`, add an optional `unusable?: boolean` prop (per contracts/components.md). When `true`: disable the "N sources" button (`openSourceEditor`, ~line 40-43) so the editor cannot be opened; disable the stale-sources banner's "Update binding" button (~line 75-85, prevent `setEditingSources(true)`); and force the embedded `DataBindSelector` (~line 91) and "Save sources" button (~line 97-105) to `disabled` if `editingSources` is already `true`. This intentionally disables the same control that today lets a user fix `staleSources` — per clarification, an unusable analysis has no in-app recovery path (FR-009/FR-011/FR-014).
|
| 97 |
+
- [X] T016 [US3] In `src/app/components/analysis/AnalysisShell.tsx`, add `const isActiveAnalysisUnusable = staleSources.length > 0;`, reusing the **already-existing** `staleSources` state (`AnalysisShell.tsx:80`, computed by the existing `useEffect` at `AnalysisShell.tsx:117-138` which already fetches `getDocuments`/`getDatabaseClients` and cross-references `activeAnalysis.data_bind`) — do **not** add a new/parallel fetch or derivation (per research.md Unknown 3 / data-model.md `isActiveAnalysisUnusable`).
|
| 98 |
+
- [X] T017 [US3] In `src/app/components/analysis/AnalysisHeader.tsx`, extend the existing stale-sources banner copy (~line 69-72, "Some bound sources are no longer available...") to also state the analysis can no longer be used (FR-010) — reuse this banner rather than adding a new one elsewhere. In `src/app/components/analysis/AnalysisShell.tsx`, pass `unusable={isActiveAnalysisUnusable}` into `<AnalysisHeader />` (~line 372). Depends on T015, T016.
|
| 99 |
+
- [X] T018 [US3] In `src/app/components/analysis/AnalysisShell.tsx`, pass `disabled={isActiveAnalysisUnusable}` into `ChatInput` (which already forwards `disabled` to its Send button, textarea, and `HelpSkillButton` per existing code), `SuggestedQuestionsBar` (from T013), and `ReportSidebar` (from T014). Depends on T013, T014, T016.
|
| 100 |
+
- [X] T019 [US3] In `src/app/components/analysis/ChatInput.tsx`, confirm the existing `onKeyDown` Enter-to-submit path already respects `disabled` via `canSend` (it does — `canSend = message.trim().length > 0 && !disabled && !streaming`); no code change expected, verify only (per FR-013).
|
| 101 |
+
- [ ] T020 [US3] Manually verify per quickstart.md § User Story 3 — **NOT YET DONE, requires a human/browser pass**: unusable banner appears (reusing the stale-sources banner); chat input/send/help/suggested-questions/generate-report are all disabled; the "N sources"/"Update binding" controls and embedded `DataBindSelector` are all disabled with no way to open or submit the source editor (FR-014); chat history and prior reports remain fully viewable; an analysis with multiple bound sources where only one was deleted is still flagged unusable; an analysis with no bound sources is unaffected; the sidebar (`listAnalyses({ status: "active" })`) still lists the unusable analysis, confirming `status` was not repurposed. Depends on T017, T018, T019.
|
| 102 |
+
|
| 103 |
+
**Checkpoint**: Unusable-analysis flagging and action-disabling fully functional and independently testable per quickstart.md § User Story 3.
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
## Phase 6: Polish & Cross-Cutting Concerns
|
| 108 |
+
|
| 109 |
+
**Purpose**: Final validation across all three stories together.
|
| 110 |
+
|
| 111 |
+
- [ ] T021 Run the full manual verification checklist in `quickstart.md` end-to-end (US1 → US3) against `npm run dev` (or `pnpm dev`), confirming no regressions between stories (e.g., an analysis flagged unusable by US3 still shows correctly in US1's impact-preview list if further sources bound to it are deleted).
|
| 112 |
+
- [ ] T022 [P] Spot-check the mobile drawer/bottom-sheet variants of `KnowledgeManagement.tsx` (`variant="modal"` vs. `"page"`) and `AnalysisShell.tsx`'s mobile report panel still render the new `AlertDialog` and unusable banner correctly at mobile viewport widths.
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
## Dependencies & Execution Order
|
| 117 |
+
|
| 118 |
+
### Phase Dependencies
|
| 119 |
+
|
| 120 |
+
- **Setup (Phase 1)**: No dependencies — can start immediately. Only blocks US1 (T004–T007 need the `AlertDialog` primitive from T001).
|
| 121 |
+
- **Foundational (Phase 2)**: N/A — no blocking work.
|
| 122 |
+
- **User Stories (Phase 3–5)**: US1 depends on Phase 1 (T001). US2 and US3 have no dependency on Phase 1 or on each other and can start immediately in parallel with US1.
|
| 123 |
+
- **Polish (Phase 6)**: Depends on all three user stories being complete.
|
| 124 |
+
|
| 125 |
+
### User Story Dependencies
|
| 126 |
+
|
| 127 |
+
- **US1 (P1)**: Depends only on Setup (T001, for `AlertDialog`). Touches `KnowledgeManagement.tsx` only.
|
| 128 |
+
- **US2 (P2)**: Fully independent — touches `NewAnalysisDialog.tsx` and one prop-passing line in `AnalysisShell.tsx` (T011).
|
| 129 |
+
- **US3 (P1)**: Fully independent of US1/US2 in intent (it only requires that *some* deletion has happened, which the app can already do today even before US1 ships) — touches `SuggestedQuestionsBar.tsx`, `ReportSidebar.tsx`, `AnalysisHeader.tsx`, and `AnalysisShell.tsx` (T016, T017, T018, sequential same-file edits; also share `AnalysisShell.tsx` with US2's T011, so apply T011 before T016–T018 if working in the same branch to avoid merge conflicts).
|
| 130 |
+
|
| 131 |
+
### Parallel Opportunities
|
| 132 |
+
|
| 133 |
+
- T005 and T006 (`KnowledgeManagement.tsx` database/clear-all delete triggers) can run in parallel with T004 (document delete trigger) — same file, but non-overlapping code regions; sequence by ID if working solo.
|
| 134 |
+
- T009 (`NewAnalysisDialog.tsx`) can run any time, fully parallel with all of US1 and US3.
|
| 135 |
+
- T013, T014, T015 (`SuggestedQuestionsBar.tsx`, `ReportSidebar.tsx`, `AnalysisHeader.tsx`) can all run in parallel with each other and with US1/US2 — three different files, no shared state.
|
| 136 |
+
- T022 can run in parallel with T021.
|
| 137 |
+
|
| 138 |
+
---
|
| 139 |
+
|
| 140 |
+
## Parallel Example: Cross-story parallel batch (once Setup/T001 is done)
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
# These touch entirely different files and can be worked on simultaneously:
|
| 144 |
+
Task: "Add existingAnalyses prop + duplicate-name guard to NewAnalysisDialog.tsx (T009, T010)"
|
| 145 |
+
Task: "Add disabled prop to SuggestedQuestionsBar.tsx (T013)"
|
| 146 |
+
Task: "Add disabled prop to ReportSidebar.tsx's generate-report button (T014)"
|
| 147 |
+
Task: "Add unusable prop to AnalysisHeader.tsx, disabling its source-editor controls (T015)"
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
---
|
| 151 |
+
|
| 152 |
+
## Implementation Strategy
|
| 153 |
+
|
| 154 |
+
### MVP First (User Story 1 Only)
|
| 155 |
+
|
| 156 |
+
1. Complete Phase 1: Setup (T001).
|
| 157 |
+
2. Complete Phase 3: User Story 1 (deletion confirmation + impact preview).
|
| 158 |
+
3. **STOP and VALIDATE**: Run quickstart.md § User Story 1 independently.
|
| 159 |
+
4. Deploy/demo if ready — this alone closes the biggest accidental-data-loss risk.
|
| 160 |
+
|
| 161 |
+
### Incremental Delivery
|
| 162 |
+
|
| 163 |
+
1. Setup (T001) → done.
|
| 164 |
+
2. US1 (Phase 3) → validate → ship (MVP).
|
| 165 |
+
3. US3 (Phase 5) → validate → ship (closes the loop opened by US1: analyses whose sources were just deleted become properly unusable).
|
| 166 |
+
4. US2 (Phase 4) → validate → ship.
|
| 167 |
+
5. Polish (Phase 6) → final cross-story pass.
|
| 168 |
+
|
| 169 |
+
Each story adds value without breaking previous stories. US1 and US3 are naturally paired (US1 makes deletion safe to do; US3 makes the consequence of doing it visible and enforced), but both can also ship independently since the app can already delete sources today.
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
## Notes
|
| 174 |
+
|
| 175 |
+
- No automated tests exist in this project; all verification is manual via `quickstart.md`.
|
| 176 |
+
- [P] tasks = different files, no dependencies.
|
| 177 |
+
- [Story] label maps task to specific user story for traceability.
|
| 178 |
+
- `AnalysisShell.tsx` is shared across US2 (T011) and US3 (T016, T017, T018) — respect task ID ordering there even when working in parallel on other files.
|
| 179 |
+
- T015/T017's disabling of `AnalysisHeader.tsx`'s "Update binding" control is intentional (per clarification, an unusable analysis has no in-app recovery path) — do not "fix" this into an exception during implementation.
|
| 180 |
+
- `KnowledgeManagement.tsx` is shared across all of US1's tasks (T002–T007) — apply in ID order even if split across contributors.
|
| 181 |
+
- Commit after each task or logical group.
|
| 182 |
+
- Stop at any checkpoint to validate a story independently.
|
src/app/components/KnowledgeManagement.tsx
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
| 27 |
ingestDatabaseClient,
|
| 28 |
getDataCatalog,
|
| 29 |
rebuildDataCatalog,
|
|
|
|
| 30 |
type ApiDocument,
|
| 31 |
type DocumentStatus,
|
| 32 |
type DocTypeInfo,
|
|
@@ -35,6 +36,16 @@ import {
|
|
| 35 |
type DatabaseClient,
|
| 36 |
type DataCatalogSource,
|
| 37 |
} from "../../services/orchestrationApi";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
interface KnowledgeManagementProps {
|
| 40 |
open: boolean;
|
|
@@ -44,6 +55,16 @@ interface KnowledgeManagementProps {
|
|
| 44 |
|
| 45 |
type View = "main" | "db-select" | "db-credentials" | "catalog";
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
const LOGO_MAP: Record<string, string> = {
|
| 48 |
postgres: "https://cdn.simpleicons.org/postgresql/336791",
|
| 49 |
mysql: "https://cdn.simpleicons.org/mysql/4479A1",
|
|
@@ -85,6 +106,12 @@ export default function KnowledgeManagement({
|
|
| 85 |
const [processing, setProcessing] = useState<string | null>(null);
|
| 86 |
const [deleting, setDeleting] = useState<string | null>(null);
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
// -- Navigation state --------------------------------------------------------
|
| 89 |
const [view, setView] = useState<View>("main");
|
| 90 |
const [selectedDbType, setSelectedDbType] = useState<DbType | null>(null);
|
|
@@ -263,8 +290,6 @@ export default function KnowledgeManagement({
|
|
| 263 |
};
|
| 264 |
|
| 265 |
const deleteAllDocuments = async () => {
|
| 266 |
-
if (!window.confirm("Are you sure you want to delete all documents?"))
|
| 267 |
-
return;
|
| 268 |
const userId = getUserId();
|
| 269 |
if (!userId) return;
|
| 270 |
for (const doc of documents) {
|
|
@@ -277,6 +302,65 @@ export default function KnowledgeManagement({
|
|
| 277 |
setDocuments([]);
|
| 278 |
};
|
| 279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
// -- DB handlers -------------------------------------------------------------
|
| 281 |
|
| 282 |
const handleDbConnect = async () => {
|
|
@@ -446,6 +530,7 @@ export default function KnowledgeManagement({
|
|
| 446 |
// -- Render -------------------------------------------------------------------
|
| 447 |
|
| 448 |
return (
|
|
|
|
| 449 |
<div className={isPage ? "knowledge-surface h-full min-h-screen bg-slate-50" : "knowledge-surface fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4 backdrop-blur-sm"}>
|
| 450 |
<div className={isPage ? "flex h-full min-h-screen w-full flex-col bg-white" : "flex max-h-[85vh] w-full max-w-md flex-col rounded-2xl bg-white shadow-xl"}>
|
| 451 |
|
|
@@ -614,7 +699,7 @@ export default function KnowledgeManagement({
|
|
| 614 |
)}
|
| 615 |
</div>
|
| 616 |
<button
|
| 617 |
-
onClick={() =>
|
| 618 |
disabled={deletingClient === client.id}
|
| 619 |
className="flex h-8 w-8 items-center justify-center rounded-lg text-slate-300 opacity-100 transition hover:bg-red-50 hover:text-red-500 disabled:opacity-30 sm:h-auto sm:w-auto sm:opacity-0 sm:group-hover:opacity-100"
|
| 620 |
title="Delete connection"
|
|
@@ -640,7 +725,7 @@ export default function KnowledgeManagement({
|
|
| 640 |
</span>
|
| 641 |
{documents.length > 0 && (
|
| 642 |
<button
|
| 643 |
-
onClick={
|
| 644 |
className="text-xs text-slate-400 hover:text-red-500 flex items-center gap-1 transition"
|
| 645 |
>
|
| 646 |
<Trash2 className="w-3 h-3" />
|
|
@@ -684,7 +769,7 @@ export default function KnowledgeManagement({
|
|
| 684 |
<div className="flex flex-shrink-0 items-center gap-1.5 sm:gap-2">
|
| 685 |
{renderStatus(doc)}
|
| 686 |
<button
|
| 687 |
-
onClick={() =>
|
| 688 |
disabled={deleting === doc.id}
|
| 689 |
className="flex h-8 w-8 items-center justify-center rounded-lg text-slate-300 opacity-100 transition hover:bg-red-50 hover:text-red-500 disabled:opacity-30 sm:h-auto sm:w-auto sm:opacity-0 sm:group-hover:opacity-100"
|
| 690 |
title="Delete"
|
|
@@ -924,5 +1009,58 @@ export default function KnowledgeManagement({
|
|
| 924 |
|
| 925 |
</div>
|
| 926 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 927 |
);
|
| 928 |
}
|
|
|
|
| 27 |
ingestDatabaseClient,
|
| 28 |
getDataCatalog,
|
| 29 |
rebuildDataCatalog,
|
| 30 |
+
listAnalyses,
|
| 31 |
type ApiDocument,
|
| 32 |
type DocumentStatus,
|
| 33 |
type DocTypeInfo,
|
|
|
|
| 36 |
type DatabaseClient,
|
| 37 |
type DataCatalogSource,
|
| 38 |
} from "../../services/orchestrationApi";
|
| 39 |
+
import {
|
| 40 |
+
AlertDialog,
|
| 41 |
+
AlertDialogAction,
|
| 42 |
+
AlertDialogCancel,
|
| 43 |
+
AlertDialogContent,
|
| 44 |
+
AlertDialogDescription,
|
| 45 |
+
AlertDialogFooter,
|
| 46 |
+
AlertDialogHeader,
|
| 47 |
+
AlertDialogTitle,
|
| 48 |
+
} from "./ui/alert-dialog";
|
| 49 |
|
| 50 |
interface KnowledgeManagementProps {
|
| 51 |
open: boolean;
|
|
|
|
| 55 |
|
| 56 |
type View = "main" | "db-select" | "db-credentials" | "catalog";
|
| 57 |
|
| 58 |
+
type DeleteIntent =
|
| 59 |
+
| { kind: "document"; docId: string; docName: string }
|
| 60 |
+
| { kind: "database"; clientId: string; clientName: string }
|
| 61 |
+
| { kind: "clear-all-documents" };
|
| 62 |
+
|
| 63 |
+
interface ImpactedAnalysis {
|
| 64 |
+
id: string;
|
| 65 |
+
analysis_title: string;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
const LOGO_MAP: Record<string, string> = {
|
| 69 |
postgres: "https://cdn.simpleicons.org/postgresql/336791",
|
| 70 |
mysql: "https://cdn.simpleicons.org/mysql/4479A1",
|
|
|
|
| 106 |
const [processing, setProcessing] = useState<string | null>(null);
|
| 107 |
const [deleting, setDeleting] = useState<string | null>(null);
|
| 108 |
|
| 109 |
+
// -- Deletion confirmation state ---------------------------------------------
|
| 110 |
+
const [deleteIntent, setDeleteIntent] = useState<DeleteIntent | null>(null);
|
| 111 |
+
const [impact, setImpact] = useState<ImpactedAnalysis[]>([]);
|
| 112 |
+
const [loadingImpact, setLoadingImpact] = useState(false);
|
| 113 |
+
const [confirming, setConfirming] = useState(false);
|
| 114 |
+
|
| 115 |
// -- Navigation state --------------------------------------------------------
|
| 116 |
const [view, setView] = useState<View>("main");
|
| 117 |
const [selectedDbType, setSelectedDbType] = useState<DbType | null>(null);
|
|
|
|
| 290 |
};
|
| 291 |
|
| 292 |
const deleteAllDocuments = async () => {
|
|
|
|
|
|
|
| 293 |
const userId = getUserId();
|
| 294 |
if (!userId) return;
|
| 295 |
for (const doc of documents) {
|
|
|
|
| 302 |
setDocuments([]);
|
| 303 |
};
|
| 304 |
|
| 305 |
+
// -- Deletion confirmation ----------------------------------------------------
|
| 306 |
+
|
| 307 |
+
const computeImpact = async (sourceIds: string[]) => {
|
| 308 |
+
setLoadingImpact(true);
|
| 309 |
+
try {
|
| 310 |
+
const result = await listAnalyses({ status: "active", limit: 200 });
|
| 311 |
+
const affected = result.analyses.filter((analysis) =>
|
| 312 |
+
analysis.data_bind.some((item) => sourceIds.includes(item.id))
|
| 313 |
+
);
|
| 314 |
+
const deduped = Array.from(new Map(affected.map((a) => [a.id, a])).values()).map(
|
| 315 |
+
(a) => ({ id: a.id, analysis_title: a.analysis_title })
|
| 316 |
+
);
|
| 317 |
+
setImpact(deduped);
|
| 318 |
+
} catch {
|
| 319 |
+
// non-blocking; if we can't compute impact, still allow the dialog to show with an empty list
|
| 320 |
+
setImpact([]);
|
| 321 |
+
} finally {
|
| 322 |
+
setLoadingImpact(false);
|
| 323 |
+
}
|
| 324 |
+
};
|
| 325 |
+
|
| 326 |
+
const requestDeleteDocument = (doc: ApiDocument) => {
|
| 327 |
+
setDeleteIntent({ kind: "document", docId: doc.id, docName: doc.filename });
|
| 328 |
+
void computeImpact([doc.id]);
|
| 329 |
+
};
|
| 330 |
+
|
| 331 |
+
const requestDeleteClient = (client: DatabaseClient) => {
|
| 332 |
+
setDeleteIntent({ kind: "database", clientId: client.id, clientName: client.name });
|
| 333 |
+
void computeImpact([client.id]);
|
| 334 |
+
};
|
| 335 |
+
|
| 336 |
+
const requestClearAllDocuments = () => {
|
| 337 |
+
setDeleteIntent({ kind: "clear-all-documents" });
|
| 338 |
+
void computeImpact(documents.map((d) => d.id));
|
| 339 |
+
};
|
| 340 |
+
|
| 341 |
+
const cancelDelete = () => {
|
| 342 |
+
setDeleteIntent(null);
|
| 343 |
+
setImpact([]);
|
| 344 |
+
};
|
| 345 |
+
|
| 346 |
+
const confirmDelete = async () => {
|
| 347 |
+
if (!deleteIntent) return;
|
| 348 |
+
setConfirming(true);
|
| 349 |
+
try {
|
| 350 |
+
if (deleteIntent.kind === "document") {
|
| 351 |
+
await handleDeleteDocument(deleteIntent.docId);
|
| 352 |
+
} else if (deleteIntent.kind === "database") {
|
| 353 |
+
await handleDeleteClient(deleteIntent.clientId);
|
| 354 |
+
} else {
|
| 355 |
+
await deleteAllDocuments();
|
| 356 |
+
}
|
| 357 |
+
} finally {
|
| 358 |
+
setConfirming(false);
|
| 359 |
+
setDeleteIntent(null);
|
| 360 |
+
setImpact([]);
|
| 361 |
+
}
|
| 362 |
+
};
|
| 363 |
+
|
| 364 |
// -- DB handlers -------------------------------------------------------------
|
| 365 |
|
| 366 |
const handleDbConnect = async () => {
|
|
|
|
| 530 |
// -- Render -------------------------------------------------------------------
|
| 531 |
|
| 532 |
return (
|
| 533 |
+
<>
|
| 534 |
<div className={isPage ? "knowledge-surface h-full min-h-screen bg-slate-50" : "knowledge-surface fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4 backdrop-blur-sm"}>
|
| 535 |
<div className={isPage ? "flex h-full min-h-screen w-full flex-col bg-white" : "flex max-h-[85vh] w-full max-w-md flex-col rounded-2xl bg-white shadow-xl"}>
|
| 536 |
|
|
|
|
| 699 |
)}
|
| 700 |
</div>
|
| 701 |
<button
|
| 702 |
+
onClick={() => requestDeleteClient(client)}
|
| 703 |
disabled={deletingClient === client.id}
|
| 704 |
className="flex h-8 w-8 items-center justify-center rounded-lg text-slate-300 opacity-100 transition hover:bg-red-50 hover:text-red-500 disabled:opacity-30 sm:h-auto sm:w-auto sm:opacity-0 sm:group-hover:opacity-100"
|
| 705 |
title="Delete connection"
|
|
|
|
| 725 |
</span>
|
| 726 |
{documents.length > 0 && (
|
| 727 |
<button
|
| 728 |
+
onClick={requestClearAllDocuments}
|
| 729 |
className="text-xs text-slate-400 hover:text-red-500 flex items-center gap-1 transition"
|
| 730 |
>
|
| 731 |
<Trash2 className="w-3 h-3" />
|
|
|
|
| 769 |
<div className="flex flex-shrink-0 items-center gap-1.5 sm:gap-2">
|
| 770 |
{renderStatus(doc)}
|
| 771 |
<button
|
| 772 |
+
onClick={() => requestDeleteDocument(doc)}
|
| 773 |
disabled={deleting === doc.id}
|
| 774 |
className="flex h-8 w-8 items-center justify-center rounded-lg text-slate-300 opacity-100 transition hover:bg-red-50 hover:text-red-500 disabled:opacity-30 sm:h-auto sm:w-auto sm:opacity-0 sm:group-hover:opacity-100"
|
| 775 |
title="Delete"
|
|
|
|
| 1009 |
|
| 1010 |
</div>
|
| 1011 |
</div>
|
| 1012 |
+
|
| 1013 |
+
<AlertDialog open={deleteIntent !== null} onOpenChange={(open) => { if (!open) cancelDelete(); }}>
|
| 1014 |
+
<AlertDialogContent>
|
| 1015 |
+
<AlertDialogHeader>
|
| 1016 |
+
<AlertDialogTitle>
|
| 1017 |
+
{deleteIntent?.kind === "document" && `Delete "${deleteIntent.docName}"?`}
|
| 1018 |
+
{deleteIntent?.kind === "database" && `Delete "${deleteIntent.clientName}"?`}
|
| 1019 |
+
{deleteIntent?.kind === "clear-all-documents" && "Delete all documents?"}
|
| 1020 |
+
</AlertDialogTitle>
|
| 1021 |
+
<AlertDialogDescription asChild>
|
| 1022 |
+
<div className="space-y-2 text-left">
|
| 1023 |
+
{loadingImpact ? (
|
| 1024 |
+
<div className="flex items-center gap-2 text-slate-500">
|
| 1025 |
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
| 1026 |
+
Checking which analyses use this source...
|
| 1027 |
+
</div>
|
| 1028 |
+
) : impact.length === 0 ? (
|
| 1029 |
+
<p>No analyses will be affected by this deletion.</p>
|
| 1030 |
+
) : (
|
| 1031 |
+
<>
|
| 1032 |
+
<p>
|
| 1033 |
+
The following analys{impact.length === 1 ? "is is" : "es are"} using this source and will no longer be usable after deletion:
|
| 1034 |
+
</p>
|
| 1035 |
+
<div className="max-h-40 overflow-y-auto rounded-md border border-slate-200 bg-slate-50">
|
| 1036 |
+
<ul className="divide-y divide-slate-200">
|
| 1037 |
+
{impact.map((analysis) => (
|
| 1038 |
+
<li key={analysis.id} className="px-3 py-2 text-sm text-slate-700 truncate">
|
| 1039 |
+
{analysis.analysis_title}
|
| 1040 |
+
</li>
|
| 1041 |
+
))}
|
| 1042 |
+
</ul>
|
| 1043 |
+
</div>
|
| 1044 |
+
</>
|
| 1045 |
+
)}
|
| 1046 |
+
</div>
|
| 1047 |
+
</AlertDialogDescription>
|
| 1048 |
+
</AlertDialogHeader>
|
| 1049 |
+
<AlertDialogFooter>
|
| 1050 |
+
<AlertDialogCancel onClick={cancelDelete}>Cancel</AlertDialogCancel>
|
| 1051 |
+
<AlertDialogAction
|
| 1052 |
+
onClick={(e) => {
|
| 1053 |
+
e.preventDefault();
|
| 1054 |
+
void confirmDelete();
|
| 1055 |
+
}}
|
| 1056 |
+
disabled={confirming}
|
| 1057 |
+
className="bg-red-600 text-white hover:bg-red-700"
|
| 1058 |
+
>
|
| 1059 |
+
{confirming ? "Deleting..." : "Delete"}
|
| 1060 |
+
</AlertDialogAction>
|
| 1061 |
+
</AlertDialogFooter>
|
| 1062 |
+
</AlertDialogContent>
|
| 1063 |
+
</AlertDialog>
|
| 1064 |
+
</>
|
| 1065 |
);
|
| 1066 |
}
|
src/app/components/analysis/AnalysisHeader.tsx
CHANGED
|
@@ -7,9 +7,10 @@ interface AnalysisHeaderProps {
|
|
| 7 |
analysis: Analysis | null;
|
| 8 |
staleSources?: DataBindItem[];
|
| 9 |
onUpdateDataBind: (items: DataBindItem[]) => Promise<void>;
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
-
export function AnalysisHeader({ analysis, staleSources = [], onUpdateDataBind }: AnalysisHeaderProps) {
|
| 13 |
const [editingSources, setEditingSources] = useState(false);
|
| 14 |
const [draftBind, setDraftBind] = useState<DataBindItem[]>(analysis?.data_bind ?? []);
|
| 15 |
const [saving, setSaving] = useState(false);
|
|
@@ -38,6 +39,7 @@ export function AnalysisHeader({ analysis, staleSources = [], onUpdateDataBind }
|
|
| 38 |
};
|
| 39 |
|
| 40 |
const openSourceEditor = () => {
|
|
|
|
| 41 |
setDraftBind(analysis.data_bind);
|
| 42 |
setEditingSources((open) => !open);
|
| 43 |
};
|
|
@@ -51,9 +53,10 @@ export function AnalysisHeader({ analysis, staleSources = [], onUpdateDataBind }
|
|
| 51 |
<div className="flex flex-shrink-0 items-center gap-2 text-xs text-slate-500">
|
| 52 |
<button
|
| 53 |
type="button"
|
| 54 |
-
title="Update bound knowledge sources for this analysis"
|
| 55 |
onClick={openSourceEditor}
|
| 56 |
-
|
|
|
|
| 57 |
>
|
| 58 |
<Database className="h-3.5 w-3.5" />
|
| 59 |
{analysis.data_bind.length} source{analysis.data_bind.length !== 1 ? "s" : ""}
|
|
@@ -66,27 +69,35 @@ export function AnalysisHeader({ analysis, staleSources = [], onUpdateDataBind }
|
|
| 66 |
<div className="flex items-start gap-2">
|
| 67 |
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
| 68 |
<div>
|
| 69 |
-
<p className="font-medium">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
<p className="mt-1 text-xs leading-5 text-amber-800">
|
| 71 |
-
|
|
|
|
|
|
|
| 72 |
</p>
|
| 73 |
</div>
|
| 74 |
</div>
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
| 86 |
</div>
|
| 87 |
)}
|
| 88 |
|
| 89 |
-
{editingSources && (
|
| 90 |
<div className="mt-4 rounded-lg border border-slate-200 bg-slate-50 p-3">
|
| 91 |
<DataBindSelector value={draftBind} onChange={setDraftBind} disabled={saving} />
|
| 92 |
{error && <p className="mt-2 text-xs text-red-600">{error}</p>}
|
|
|
|
| 7 |
analysis: Analysis | null;
|
| 8 |
staleSources?: DataBindItem[];
|
| 9 |
onUpdateDataBind: (items: DataBindItem[]) => Promise<void>;
|
| 10 |
+
unusable?: boolean;
|
| 11 |
}
|
| 12 |
|
| 13 |
+
export function AnalysisHeader({ analysis, staleSources = [], onUpdateDataBind, unusable = false }: AnalysisHeaderProps) {
|
| 14 |
const [editingSources, setEditingSources] = useState(false);
|
| 15 |
const [draftBind, setDraftBind] = useState<DataBindItem[]>(analysis?.data_bind ?? []);
|
| 16 |
const [saving, setSaving] = useState(false);
|
|
|
|
| 39 |
};
|
| 40 |
|
| 41 |
const openSourceEditor = () => {
|
| 42 |
+
if (unusable) return;
|
| 43 |
setDraftBind(analysis.data_bind);
|
| 44 |
setEditingSources((open) => !open);
|
| 45 |
};
|
|
|
|
| 53 |
<div className="flex flex-shrink-0 items-center gap-2 text-xs text-slate-500">
|
| 54 |
<button
|
| 55 |
type="button"
|
| 56 |
+
title={unusable ? "Bound sources can no longer be updated for this analysis" : "Update bound knowledge sources for this analysis"}
|
| 57 |
onClick={openSourceEditor}
|
| 58 |
+
disabled={unusable}
|
| 59 |
+
className="inline-flex items-center gap-1 rounded-md border border-slate-200 px-2 py-1 hover:bg-slate-50 hover:text-slate-800 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-slate-500"
|
| 60 |
>
|
| 61 |
<Database className="h-3.5 w-3.5" />
|
| 62 |
{analysis.data_bind.length} source{analysis.data_bind.length !== 1 ? "s" : ""}
|
|
|
|
| 69 |
<div className="flex items-start gap-2">
|
| 70 |
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0" />
|
| 71 |
<div>
|
| 72 |
+
<p className="font-medium">
|
| 73 |
+
{unusable
|
| 74 |
+
? "Some bound sources are no longer available — this analysis can no longer be used."
|
| 75 |
+
: "Some bound sources are no longer available."}
|
| 76 |
+
</p>
|
| 77 |
<p className="mt-1 text-xs leading-5 text-amber-800">
|
| 78 |
+
{unusable
|
| 79 |
+
? `Missing sources: ${staleSources.map((source) => source.name).join(", ")}. Chat, help, suggested questions, report generation, and source updates are disabled for this analysis. You can still view its chat history and previously generated reports.`
|
| 80 |
+
: `Update sources before relying on this analysis: ${staleSources.map((source) => source.name).join(", ")}.`}
|
| 81 |
</p>
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
+
{!unusable && (
|
| 85 |
+
<button
|
| 86 |
+
type="button"
|
| 87 |
+
title="Open source binding editor"
|
| 88 |
+
onClick={() => {
|
| 89 |
+
setDraftBind(analysis.data_bind);
|
| 90 |
+
setEditingSources(true);
|
| 91 |
+
}}
|
| 92 |
+
className="rounded-md bg-amber-900 px-3 py-2 text-xs font-medium text-white hover:bg-amber-800"
|
| 93 |
+
>
|
| 94 |
+
Update binding
|
| 95 |
+
</button>
|
| 96 |
+
)}
|
| 97 |
</div>
|
| 98 |
)}
|
| 99 |
|
| 100 |
+
{editingSources && !unusable && (
|
| 101 |
<div className="mt-4 rounded-lg border border-slate-200 bg-slate-50 p-3">
|
| 102 |
<DataBindSelector value={draftBind} onChange={setDraftBind} disabled={saving} />
|
| 103 |
{error && <p className="mt-2 text-xs text-red-600">{error}</p>}
|
src/app/components/analysis/AnalysisShell.tsx
CHANGED
|
@@ -367,9 +367,16 @@ export function AnalysisShell() {
|
|
| 367 |
const activeMenuLabel =
|
| 368 |
activeMenu === "analysis-agent" ? "Analysis Agent" : activeMenu === "knowledge" ? "Knowledge" : "Home";
|
| 369 |
|
|
|
|
|
|
|
| 370 |
const chatPane = (
|
| 371 |
<main className="flex h-full min-h-0 min-w-0 flex-col bg-slate-50">
|
| 372 |
-
<AnalysisHeader
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain">
|
| 374 |
{loadingMessages ? (
|
| 375 |
<div className="flex h-full items-center justify-center text-sm text-slate-500">Loading messages</div>
|
|
@@ -384,9 +391,18 @@ export function AnalysisShell() {
|
|
| 384 |
)}
|
| 385 |
</div>
|
| 386 |
{messages.length > 0 && (
|
| 387 |
-
<SuggestedQuestionsBar
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
)}
|
| 389 |
-
<ChatInput
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
</main>
|
| 391 |
);
|
| 392 |
|
|
@@ -410,6 +426,7 @@ export function AnalysisShell() {
|
|
| 410 |
onCollapse={() => setReportCollapsed(true)}
|
| 411 |
fullscreen={reportFullscreen}
|
| 412 |
onToggleFullscreen={() => setReportFullscreen((value) => !value)}
|
|
|
|
| 413 |
/>
|
| 414 |
</div>
|
| 415 |
);
|
|
@@ -521,7 +538,12 @@ export function AnalysisShell() {
|
|
| 521 |
className="absolute inset-0 bg-slate-950/40"
|
| 522 |
/>
|
| 523 |
<div className="absolute inset-x-0 bottom-0 max-h-[86vh] overflow-y-auto rounded-t-xl bg-white p-4 shadow-2xl">
|
| 524 |
-
<ReportSidebar
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
</div>
|
| 526 |
</div>
|
| 527 |
)}
|
|
@@ -551,7 +573,7 @@ export function AnalysisShell() {
|
|
| 551 |
</div>
|
| 552 |
</div>
|
| 553 |
|
| 554 |
-
<NewAnalysisDialog open={newAnalysisOpen} onClose={() => setNewAnalysisOpen(false)} onCreated={handleCreated} />
|
| 555 |
</div>
|
| 556 |
);
|
| 557 |
}
|
|
|
|
| 367 |
const activeMenuLabel =
|
| 368 |
activeMenu === "analysis-agent" ? "Analysis Agent" : activeMenu === "knowledge" ? "Knowledge" : "Home";
|
| 369 |
|
| 370 |
+
const isActiveAnalysisUnusable = staleSources.length > 0;
|
| 371 |
+
|
| 372 |
const chatPane = (
|
| 373 |
<main className="flex h-full min-h-0 min-w-0 flex-col bg-slate-50">
|
| 374 |
+
<AnalysisHeader
|
| 375 |
+
analysis={activeAnalysis}
|
| 376 |
+
staleSources={staleSources}
|
| 377 |
+
onUpdateDataBind={handleUpdateDataBind}
|
| 378 |
+
unusable={isActiveAnalysisUnusable}
|
| 379 |
+
/>
|
| 380 |
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain">
|
| 381 |
{loadingMessages ? (
|
| 382 |
<div className="flex h-full items-center justify-center text-sm text-slate-500">Loading messages</div>
|
|
|
|
| 391 |
)}
|
| 392 |
</div>
|
| 393 |
{messages.length > 0 && (
|
| 394 |
+
<SuggestedQuestionsBar
|
| 395 |
+
questions={activeAnalysis?.business_questions ?? []}
|
| 396 |
+
onSelect={handleSend}
|
| 397 |
+
disabled={isActiveAnalysisUnusable}
|
| 398 |
+
/>
|
| 399 |
)}
|
| 400 |
+
<ChatInput
|
| 401 |
+
disabled={!activeAnalysis || isActiveAnalysisUnusable}
|
| 402 |
+
streaming={streamState !== "idle"}
|
| 403 |
+
onSend={handleSend}
|
| 404 |
+
onHelp={handleHelp}
|
| 405 |
+
/>
|
| 406 |
</main>
|
| 407 |
);
|
| 408 |
|
|
|
|
| 426 |
onCollapse={() => setReportCollapsed(true)}
|
| 427 |
fullscreen={reportFullscreen}
|
| 428 |
onToggleFullscreen={() => setReportFullscreen((value) => !value)}
|
| 429 |
+
disabled={isActiveAnalysisUnusable}
|
| 430 |
/>
|
| 431 |
</div>
|
| 432 |
);
|
|
|
|
| 538 |
className="absolute inset-0 bg-slate-950/40"
|
| 539 |
/>
|
| 540 |
<div className="absolute inset-x-0 bottom-0 max-h-[86vh] overflow-y-auto rounded-t-xl bg-white p-4 shadow-2xl">
|
| 541 |
+
<ReportSidebar
|
| 542 |
+
analysis={activeAnalysis}
|
| 543 |
+
userId={session?.user_id}
|
| 544 |
+
onCollapse={() => setMobileReportOpen(false)}
|
| 545 |
+
disabled={isActiveAnalysisUnusable}
|
| 546 |
+
/>
|
| 547 |
</div>
|
| 548 |
</div>
|
| 549 |
)}
|
|
|
|
| 573 |
</div>
|
| 574 |
</div>
|
| 575 |
|
| 576 |
+
<NewAnalysisDialog open={newAnalysisOpen} onClose={() => setNewAnalysisOpen(false)} onCreated={handleCreated} existingAnalyses={analyses} />
|
| 577 |
</div>
|
| 578 |
);
|
| 579 |
}
|
src/app/components/analysis/NewAnalysisDialog.tsx
CHANGED
|
@@ -16,13 +16,20 @@ function formatCreateAnalysisError(err: unknown) {
|
|
| 16 |
}
|
| 17 |
return message;
|
| 18 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
interface NewAnalysisDialogProps {
|
| 20 |
open: boolean;
|
| 21 |
onClose: () => void;
|
| 22 |
onCreated: (analysis: Analysis) => void;
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
-
export function NewAnalysisDialog({ open, onClose, onCreated }: NewAnalysisDialogProps) {
|
| 26 |
const [title, setTitle] = useState("");
|
| 27 |
const [objective, setObjective] = useState("");
|
| 28 |
const [questions, setQuestions] = useState<string[]>(["", ""]);
|
|
@@ -32,7 +39,14 @@ export function NewAnalysisDialog({ open, onClose, onCreated }: NewAnalysisDialo
|
|
| 32 |
|
| 33 |
if (!open) return null;
|
| 34 |
|
| 35 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
const submit = async (event: React.FormEvent) => {
|
| 38 |
event.preventDefault();
|
|
@@ -83,6 +97,9 @@ export function NewAnalysisDialog({ open, onClose, onCreated }: NewAnalysisDialo
|
|
| 83 |
placeholder="Q3 revenue movement"
|
| 84 |
className="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100"
|
| 85 |
/>
|
|
|
|
|
|
|
|
|
|
| 86 |
</div>
|
| 87 |
|
| 88 |
<div className="space-y-1.5">
|
|
|
|
| 16 |
}
|
| 17 |
return message;
|
| 18 |
}
|
| 19 |
+
function isDuplicateName(candidate: string, existing: Analysis[]): boolean {
|
| 20 |
+
const normalized = candidate.trim().toLowerCase();
|
| 21 |
+
if (!normalized) return false;
|
| 22 |
+
return existing.some((a) => a.analysis_title.trim().toLowerCase() === normalized);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
interface NewAnalysisDialogProps {
|
| 26 |
open: boolean;
|
| 27 |
onClose: () => void;
|
| 28 |
onCreated: (analysis: Analysis) => void;
|
| 29 |
+
existingAnalyses: Analysis[];
|
| 30 |
}
|
| 31 |
|
| 32 |
+
export function NewAnalysisDialog({ open, onClose, onCreated, existingAnalyses }: NewAnalysisDialogProps) {
|
| 33 |
const [title, setTitle] = useState("");
|
| 34 |
const [objective, setObjective] = useState("");
|
| 35 |
const [questions, setQuestions] = useState<string[]>(["", ""]);
|
|
|
|
| 39 |
|
| 40 |
if (!open) return null;
|
| 41 |
|
| 42 |
+
const duplicateName = isDuplicateName(title, existingAnalyses);
|
| 43 |
+
|
| 44 |
+
const canSubmit =
|
| 45 |
+
title.trim() &&
|
| 46 |
+
!duplicateName &&
|
| 47 |
+
objective.trim() &&
|
| 48 |
+
compactQuestions(questions).length >= 2 &&
|
| 49 |
+
dataBind.length > 0;
|
| 50 |
|
| 51 |
const submit = async (event: React.FormEvent) => {
|
| 52 |
event.preventDefault();
|
|
|
|
| 97 |
placeholder="Q3 revenue movement"
|
| 98 |
className="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100"
|
| 99 |
/>
|
| 100 |
+
{duplicateName && (
|
| 101 |
+
<p className="text-xs text-red-600">An analysis with this name already exists. Choose a different name.</p>
|
| 102 |
+
)}
|
| 103 |
</div>
|
| 104 |
|
| 105 |
<div className="space-y-1.5">
|
src/app/components/analysis/ReportSidebar.tsx
CHANGED
|
@@ -20,9 +20,10 @@ interface ReportSidebarProps {
|
|
| 20 |
onCollapse?: () => void;
|
| 21 |
fullscreen?: boolean;
|
| 22 |
onToggleFullscreen?: () => void;
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
-
export function ReportSidebar({ analysis, userId, onCollapse, fullscreen = false, onToggleFullscreen }: ReportSidebarProps) {
|
| 26 |
const [versions, setVersions] = useState<ReportSummary[]>([]);
|
| 27 |
const [selectedVersion, setSelectedVersion] = useState<number | undefined>();
|
| 28 |
const [detail, setDetail] = useState<ReportDetail | null>(null);
|
|
@@ -135,7 +136,7 @@ export function ReportSidebar({ analysis, userId, onCollapse, fullscreen = false
|
|
| 135 |
aria-label="Generate report"
|
| 136 |
title="Generate a new report version with the Python report skill"
|
| 137 |
onClick={handleGenerate}
|
| 138 |
-
disabled={!analysis || !userId || generating}
|
| 139 |
className="inline-flex items-center gap-2 rounded-md bg-slate-900 px-3 py-2 text-xs font-medium text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
|
| 140 |
>
|
| 141 |
{generating && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
|
|
|
| 20 |
onCollapse?: () => void;
|
| 21 |
fullscreen?: boolean;
|
| 22 |
onToggleFullscreen?: () => void;
|
| 23 |
+
disabled?: boolean;
|
| 24 |
}
|
| 25 |
|
| 26 |
+
export function ReportSidebar({ analysis, userId, onCollapse, fullscreen = false, onToggleFullscreen, disabled = false }: ReportSidebarProps) {
|
| 27 |
const [versions, setVersions] = useState<ReportSummary[]>([]);
|
| 28 |
const [selectedVersion, setSelectedVersion] = useState<number | undefined>();
|
| 29 |
const [detail, setDetail] = useState<ReportDetail | null>(null);
|
|
|
|
| 136 |
aria-label="Generate report"
|
| 137 |
title="Generate a new report version with the Python report skill"
|
| 138 |
onClick={handleGenerate}
|
| 139 |
+
disabled={!analysis || !userId || generating || disabled}
|
| 140 |
className="inline-flex items-center gap-2 rounded-md bg-slate-900 px-3 py-2 text-xs font-medium text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
|
| 141 |
>
|
| 142 |
{generating && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
src/app/components/analysis/SuggestedQuestionsBar.tsx
CHANGED
|
@@ -4,9 +4,10 @@ import { compactQuestions } from "./utils";
|
|
| 4 |
interface SuggestedQuestionsBarProps {
|
| 5 |
questions: string[];
|
| 6 |
onSelect: (question: string) => void;
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
-
export function SuggestedQuestionsBar({ questions, onSelect }: SuggestedQuestionsBarProps) {
|
| 10 |
const filtered = compactQuestions(questions);
|
| 11 |
if (filtered.length === 0) return null;
|
| 12 |
|
|
@@ -18,9 +19,10 @@ export function SuggestedQuestionsBar({ questions, onSelect }: SuggestedQuestion
|
|
| 18 |
<button
|
| 19 |
key={question}
|
| 20 |
type="button"
|
| 21 |
-
onClick={() => onSelect(question)}
|
|
|
|
| 22 |
title={question}
|
| 23 |
-
className="flex-shrink-0 whitespace-nowrap rounded-full border border-slate-200 bg-white px-3 py-1 text-xs text-slate-600 transition hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-800"
|
| 24 |
>
|
| 25 |
{question}
|
| 26 |
</button>
|
|
|
|
| 4 |
interface SuggestedQuestionsBarProps {
|
| 5 |
questions: string[];
|
| 6 |
onSelect: (question: string) => void;
|
| 7 |
+
disabled?: boolean;
|
| 8 |
}
|
| 9 |
|
| 10 |
+
export function SuggestedQuestionsBar({ questions, onSelect, disabled }: SuggestedQuestionsBarProps) {
|
| 11 |
const filtered = compactQuestions(questions);
|
| 12 |
if (filtered.length === 0) return null;
|
| 13 |
|
|
|
|
| 19 |
<button
|
| 20 |
key={question}
|
| 21 |
type="button"
|
| 22 |
+
onClick={() => { if (!disabled) onSelect(question); }}
|
| 23 |
+
disabled={disabled}
|
| 24 |
title={question}
|
| 25 |
+
className="flex-shrink-0 whitespace-nowrap rounded-full border border-slate-200 bg-white px-3 py-1 text-xs text-slate-600 transition hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-800 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-slate-200 disabled:hover:bg-white disabled:hover:text-slate-600"
|
| 26 |
>
|
| 27 |
{question}
|
| 28 |
</button>
|