diff --git a/components/swift/artifacts/artifact-area.tsx b/components/swift/artifacts/artifact-area.tsx
index 9ba6763..85e4bcc 100644
--- a/components/swift/artifacts/artifact-area.tsx
+++ b/components/swift/artifacts/artifact-area.tsx
@@ -1,85 +1,30 @@
'use client';
/**
- * Artifact Area (v3.0)
+ * Artifact Area (v3.0 + E4)
*
- * Displays active blocks based on activeBlock state.
- * Shows placeholder when no block is active.
+ * Displays artifacts using ArtifactContainer with tab support.
+ * Manages multiple artifacts with lifecycle actions.
*
- * Epic: E1 (Foundation), E3 (Chat API & Medical Scribe)
- * Stories: E1.S3 (Placeholder componenten), E3.S6 (Artifact opening from chat)
+ * Epic: E1 (Foundation), E3 (Chat API), E4 (Artifact Area & Tabs)
+ * Stories: E1.S3, E3.S6, E4.S1, E4.S2
*/
import { useSwiftStore } from '@/stores/swift-store';
-import { DagnotatieBlock } from '../blocks/dagnotitie-block';
-import { ZoekenBlock } from '../blocks/zoeken-block';
-import { OverdrachtBlock } from '../blocks/overdracht-block';
-import { FallbackPicker } from '../blocks/fallback-picker';
+import { ArtifactContainer } from './artifact-container';
export function ArtifactArea() {
- const activeBlock = useSwiftStore((s) => s.activeBlock);
- const prefillData = useSwiftStore((s) => s.prefillData);
+ const openArtifacts = useSwiftStore((s) => s.openArtifacts);
+ const activeArtifactId = useSwiftStore((s) => s.activeArtifactId);
+ const switchArtifact = useSwiftStore((s) => s.switchArtifact);
+ const closeArtifact = useSwiftStore((s) => s.closeArtifact);
- // Render active block
- if (activeBlock) {
- return (
-
- {activeBlock === 'dagnotitie' && }
- {activeBlock === 'zoeken' && }
- {activeBlock === 'overdracht' && }
- {activeBlock === 'fallback' && }
-
- );
- }
-
- // Placeholder when no block is active
return (
-
-
-
π
-
- Artifacts verschijnen hier
-
-
- Wanneer je een actie vraagt in de chat, verschijnt het bijbehorende formulier of overzicht hier.
-
-
-
-
-
- π
-
-
-
Dagnotitie
-
Voor snelle notities tijdens de dienst
-
-
-
-
-
- π
-
-
-
PatiΓ«nt Zoeken
-
Zoek en selecteer patiΓ«nten
-
-
-
-
-
- π
-
-
-
Overdracht
-
Dienst overdracht met AI-samenvatting
-
-
-
-
-
- Vraag me iets in de chat om te beginnen!
-
-
-
+
);
}
diff --git a/components/swift/command-center/command-center.tsx b/components/swift/command-center/command-center.tsx
index 3f7673f..6f6f3ad 100644
--- a/components/swift/command-center/command-center.tsx
+++ b/components/swift/command-center/command-center.tsx
@@ -22,18 +22,19 @@ import { ContextBar } from './context-bar';
import { OfflineBanner } from './offline-banner';
import { ChatPanel } from '../chat/chat-panel';
import { ArtifactArea } from '../artifacts/artifact-area';
+import { getArtifactTitle } from '../artifacts/artifact-container';
export function CommandCenter() {
- const { closeBlock, activeBlock, openBlock, pendingAction, setPendingAction } = useSwiftStore();
+ const { closeAllArtifacts, openArtifacts, openArtifact, pendingAction, setPendingAction } = useSwiftStore();
const inputRef = useRef(null);
// Global keyboard shortcuts
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
- // Escape: close active block
- if (e.key === 'Escape' && activeBlock) {
+ // Escape: close all artifacts
+ if (e.key === 'Escape' && openArtifacts.length > 0) {
e.preventDefault();
- closeBlock();
+ closeAllArtifacts();
}
// Cmd/Ctrl + K: focus input (chat input in v3.0)
@@ -42,7 +43,7 @@ export function CommandCenter() {
inputRef.current?.focus();
}
},
- [activeBlock, closeBlock]
+ [openArtifacts, closeAllArtifacts]
);
useEffect(() => {
@@ -50,7 +51,7 @@ export function CommandCenter() {
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
- // E3.S6: Handle pending actions from chat (artifact opening)
+ // E3.S6 + E4.S2: Handle pending actions from chat (artifact opening)
useEffect(() => {
if (!pendingAction) return;
@@ -60,10 +61,17 @@ export function CommandCenter() {
if (pendingAction.artifact) {
const { type, prefill } = pendingAction.artifact;
- console.log('[CommandCenter] Opening artifact:', type, 'with prefill:', prefill);
+ // Generate title for the artifact
+ const title = getArtifactTitle(type, prefill);
- // Open the block with prefill data
- openBlock(type, prefill);
+ console.log('[CommandCenter] Opening artifact:', type, title);
+
+ // Open the artifact (E4.S2 - new artifact system)
+ openArtifact({
+ type,
+ prefill,
+ title,
+ });
// Clear pending action after processing
setPendingAction(null);
@@ -71,7 +79,7 @@ export function CommandCenter() {
console.log('[CommandCenter] Action has no artifact, skipping');
setPendingAction(null);
}
- }, [pendingAction, openBlock, setPendingAction]);
+ }, [pendingAction, openArtifact, setPendingAction]);
return (
diff --git a/docs/swift/bouwplan-swift-v3.md b/docs/swift/bouwplan-swift-v3.md
index 73be7a1..6a64529 100644
--- a/docs/swift/bouwplan-swift-v3.md
+++ b/docs/swift/bouwplan-swift-v3.md
@@ -222,12 +222,12 @@ const useChatStore = create
((set) => ({
| E1 | Foundation - Split-screen | Layout naar 40/60 split | β
**Compleet** | 3/3 | 12 SP | E1.S1 geskipt (geen feature flag) |
| E2 | Chat Panel & Messages | Chat UI zonder AI | β
**Compleet** | 5/5 | 13 SP | Scrolling, input, shortcuts |
| E3 | Chat API & Medical Scribe | AI conversatie werkend | β
**Compleet** | 6/6 | 21 SP | Artifact opening werkend! |
-| E4 | Artifact Area & Tabs | Meerdere artifacts mogelijk | β³ In Progress | 1/4 | 13 SP | Container & tabs structure |
+| E4 | Artifact Area & Tabs | Meerdere artifacts mogelijk | β³ In Progress | 2/4 | 13 SP | Lifecycle management werkend |
| E5 | AI-Filtering & Polish | Psychiater filtering, polish | β³ To Do | 0/5 | 13 SP | Week 6 |
| E6 | Testing & Refinement | QA, bugs, performance | β³ To Do | 0/4 | 8 SP | Week 7-8 |
**Totaal:** 31 stories, **85 Story Points** (~7 weken Γ 12 SP/week)
-**Voortgang:** β
17/31 stories compleet, 2 geskipt (53 SP / 85 SP = **62%**)
+**Voortgang:** β
18/31 stories compleet, 2 geskipt (56 SP / 85 SP = **66%**)
**Belangrijk:**
- β οΈ Voer niet in 1x het volledige plan uit. Bouw per epic en per story.
@@ -613,7 +613,7 @@ E3.S5 (useChatStream hook) is geskipt omdat:
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|----------|--------------|---------------------|--------|------------------|--------------|
| E4.S1 | ArtifactContainer component | Wrapper met tabs bovenaan, max 3 artifacts | β
**Compleet** | E3.S6 | 5 |
-| E4.S2 | Artifact lifecycle management | Open/close/switch tussen artifacts in store | β³ | E4.S1 | 3 |
+| E4.S2 | Artifact lifecycle management | Open/close/switch tussen artifacts in store | β
**Compleet** | E4.S1 | 3 |
| E4.S3 | Slide-in animatie | Artifact slide-in van rechts (200ms ease-out) | β³ | E4.S2 | 2 |
| E4.S4 | Placeholder state | "Artifacts verschijnen hier" met voorbeelden | β³ | E4.S3 | 3 |
@@ -739,7 +739,27 @@ function ArtifactPlaceholder() {
**Git Commits:**
- (to be committed) β E4.S1 (ArtifactContainer component)
-**Note:** E4.S1 definieert de structuur. E4.S2 voegt store state toe (openArtifacts[], activeArtifactId, actions).
+**Deliverables (E4.S2 compleet):**
+- β
`stores/swift-store.ts` β openArtifacts[], activeArtifactId state
+- β
Store actions: openArtifact(), closeArtifact(), switchArtifact(), closeAllArtifacts()
+- β
openArtifact: Auto-generate ID + timestamp, max 3 logic (remove oldest)
+- β
closeArtifact: Filter out artifact, auto-switch to last remaining
+- β
switchArtifact: Verify exists, set activeArtifactId
+- β
closeAllArtifacts: Clear all artifacts + active ID
+- β
Console logging: "[Store] Opening artifact:", "[Store] Closing artifact:", etc.
+- β
ArtifactArea: Refactored to use ArtifactContainer
+- β
ArtifactArea: Pass store actions (switchArtifact, closeArtifact)
+- β
CommandCenter: openArtifact() i.p.v. openBlock()
+- β
CommandCenter: getArtifactTitle() voor artifact titles
+- β
CommandCenter: Escape key β closeAllArtifacts()
+- β
Integration: Chat β Action β openArtifact β Tabs + Container
+- β
Build succesvol zonder errors
+
+**Git Commits:**
+- `855744c` β E4.S1 (ArtifactContainer component)
+- (to be committed) β E4.S2 (Artifact lifecycle management)
+
+**Note:** E4.S1 + E4.S2 maken het artifact systeem volledig functioneel. E4.S3 voegt animaties toe.
**Deliverable Epic 4 complete:** Meerdere artifacts mogelijk, smooth transitions, placeholder state
diff --git a/stores/swift-store.ts b/stores/swift-store.ts
index 5881848..9c128b7 100644
--- a/stores/swift-store.ts
+++ b/stores/swift-store.ts
@@ -92,15 +92,25 @@ interface SwiftStore {
isStreaming: boolean;
pendingAction: ChatAction | null;
+ // Artifact state (E4 - Multiple artifacts support)
+ openArtifacts: Artifact[];
+ activeArtifactId: string | null;
+
// Context actions
setActivePatient: (patient: Patient | null) => void;
setShift: (shift: ShiftType) => void;
- // Block actions
+ // Block actions (legacy - will be replaced by artifact actions)
openBlock: (type: BlockType, prefill?: BlockPrefillData) => void;
closeBlock: () => void;
setBlockLoading: (loading: boolean) => void;
+ // Artifact actions (E4.S2)
+ openArtifact: (artifact: Omit) => void;
+ closeArtifact: (id: string) => void;
+ switchArtifact: (id: string) => void;
+ closeAllArtifacts: () => void;
+
// Input actions
setInputValue: (value: string) => void;
setVoiceActive: (active: boolean) => void;
@@ -143,6 +153,9 @@ const initialState = {
chatMessages: [],
isStreaming: false,
pendingAction: null,
+ // Artifact state (E4)
+ openArtifacts: [],
+ activeArtifactId: null,
};
// Create the store
@@ -264,6 +277,89 @@ export const useSwiftStore = create()(
setPendingAction: (action) => set({ pendingAction: action }, false, 'setPendingAction'),
+ // Artifact actions (E4.S2)
+ openArtifact: (artifactData) => {
+ set(
+ (state) => {
+ // Create artifact with unique ID and timestamp
+ const newArtifact: Artifact = {
+ ...artifactData,
+ id: crypto.randomUUID(),
+ createdAt: new Date(),
+ };
+
+ let artifacts = [...state.openArtifacts];
+
+ // Max 3 artifacts - remove oldest if at capacity
+ if (artifacts.length >= 3) {
+ console.log('[Store] Max 3 artifacts reached, removing oldest');
+ artifacts = artifacts.slice(1); // Remove first (oldest)
+ }
+
+ console.log('[Store] Opening artifact:', newArtifact.type, newArtifact.title);
+
+ return {
+ openArtifacts: [...artifacts, newArtifact],
+ activeArtifactId: newArtifact.id,
+ };
+ },
+ false,
+ 'openArtifact'
+ );
+ },
+
+ closeArtifact: (id) => {
+ set(
+ (state) => {
+ const artifacts = state.openArtifacts.filter((a) => a.id !== id);
+
+ // If closing active artifact, switch to last remaining artifact
+ let newActiveId = state.activeArtifactId;
+ if (state.activeArtifactId === id) {
+ newActiveId = artifacts.length > 0 ? artifacts[artifacts.length - 1].id : null;
+ }
+
+ console.log('[Store] Closing artifact:', id, 'New active:', newActiveId);
+
+ return {
+ openArtifacts: artifacts,
+ activeArtifactId: newActiveId,
+ };
+ },
+ false,
+ 'closeArtifact'
+ );
+ },
+
+ switchArtifact: (id) => {
+ set(
+ (state) => {
+ // Verify artifact exists
+ const exists = state.openArtifacts.some((a) => a.id === id);
+ if (!exists) {
+ console.warn('[Store] Cannot switch to artifact:', id, '(not found)');
+ return state;
+ }
+
+ console.log('[Store] Switching to artifact:', id);
+ return { activeArtifactId: id };
+ },
+ false,
+ 'switchArtifact'
+ );
+ },
+
+ closeAllArtifacts: () => {
+ set(
+ {
+ openArtifacts: [],
+ activeArtifactId: null,
+ },
+ false,
+ 'closeAllArtifacts'
+ );
+ },
+
// Reset
reset: () => set(initialState, false, 'reset'),
}),