coaia-visualizer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.hch/issues.json +156 -0
  2. package/.hch/issues.md +2 -0
  3. package/README.md +67 -0
  4. package/app/api/jsonl/route.ts +71 -0
  5. package/app/globals.css +125 -0
  6. package/app/layout.tsx +48 -0
  7. package/app/page.tsx +284 -0
  8. package/cli.ts +170 -0
  9. package/components/chart-detail.tsx +213 -0
  10. package/components/chart-list.tsx +184 -0
  11. package/components/data-stats.tsx +49 -0
  12. package/components/file-upload.tsx +73 -0
  13. package/components/narrative-beats.tsx +108 -0
  14. package/components/relation-graph.tsx +81 -0
  15. package/components/theme-provider.tsx +11 -0
  16. package/components/ui/badge.tsx +46 -0
  17. package/components/ui/button.tsx +60 -0
  18. package/components/ui/card.tsx +92 -0
  19. package/components/ui/scroll-area.tsx +58 -0
  20. package/components/ui/separator.tsx +28 -0
  21. package/components/ui/tabs.tsx +66 -0
  22. package/components.json +21 -0
  23. package/dist/cli.js +144 -0
  24. package/feat-2-webui-local-editing/IMPLEMENTATION.md +245 -0
  25. package/feat-2-webui-local-editing/INTEGRATION.md +302 -0
  26. package/feat-2-webui-local-editing/QUICKSTART.md +129 -0
  27. package/feat-2-webui-local-editing/README.md +254 -0
  28. package/feat-2-webui-local-editing/api-route-jsonl.ts +71 -0
  29. package/feat-2-webui-local-editing/cli.ts +170 -0
  30. package/feat-2-webui-local-editing/demo.sh +98 -0
  31. package/feat-2-webui-local-editing/package.json +82 -0
  32. package/feat-2-webui-local-editing/test-integration.sh +93 -0
  33. package/feat-2-webui-local-editing/updated-page.tsx +284 -0
  34. package/hooks/use-toast.ts +17 -0
  35. package/lib/jsonl-parser.ts +153 -0
  36. package/lib/types.ts +39 -0
  37. package/lib/utils.ts +6 -0
  38. package/next.config.mjs +12 -0
  39. package/package.json +82 -0
  40. package/postcss.config.mjs +8 -0
  41. package/public/apple-icon.png +0 -0
  42. package/public/icon-dark-32x32.png +0 -0
  43. package/public/icon-light-32x32.png +0 -0
  44. package/public/icon.svg +26 -0
  45. package/public/placeholder-logo.png +0 -0
  46. package/public/placeholder-logo.svg +1 -0
  47. package/public/placeholder-user.jpg +0 -0
  48. package/public/placeholder.jpg +0 -0
  49. package/public/placeholder.svg +1 -0
  50. package/styles/globals.css +125 -0
  51. package/tsconfig.json +41 -0
package/app/page.tsx ADDED
@@ -0,0 +1,284 @@
1
+ // app/page.tsx - Updated to support auto-loading from API
2
+ "use client"
3
+
4
+ import { useState, useEffect } from "react"
5
+ import { FileUpload } from "@/components/file-upload"
6
+ import { ChartList } from "@/components/chart-list"
7
+ import { ChartDetail } from "@/components/chart-detail"
8
+ import { DataStats } from "@/components/data-stats"
9
+ import type { ParsedData, Chart } from "@/lib/types"
10
+ import { parseJSONL, organizeData } from "@/lib/jsonl-parser"
11
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
12
+ import { Download, Upload, RefreshCw, Save } from "lucide-react"
13
+ import { Button } from "@/components/ui/button"
14
+ import { useToast } from "@/hooks/use-toast"
15
+
16
+ export default function Page() {
17
+ const [data, setData] = useState<ParsedData | null>(null)
18
+ const [selectedChart, setSelectedChart] = useState<Chart | null>(null)
19
+ const [viewMode, setViewMode] = useState<"hierarchy" | "list">("hierarchy")
20
+ const [fileName, setFileName] = useState<string>("")
21
+ const [isAutoLoaded, setIsAutoLoaded] = useState(false)
22
+ const [isLoading, setIsLoading] = useState(false)
23
+ const { toast } = useToast()
24
+
25
+ // Auto-load from API if COAIAV_MEMORY_PATH is set
26
+ useEffect(() => {
27
+ async function autoLoad() {
28
+ try {
29
+ const response = await fetch('/api/jsonl')
30
+ if (response.ok) {
31
+ const { content, filename } = await response.json()
32
+ handleFileLoad(content, filename)
33
+ setIsAutoLoaded(true)
34
+ toast({
35
+ title: "File loaded",
36
+ description: `Loaded ${filename} from local filesystem`,
37
+ })
38
+ }
39
+ } catch (error) {
40
+ console.log('No auto-load available, waiting for manual upload')
41
+ }
42
+ }
43
+ autoLoad()
44
+ }, [])
45
+
46
+ const handleFileLoad = (content: string, name: string) => {
47
+ try {
48
+ const records = parseJSONL(content)
49
+ const organized = organizeData(records)
50
+ setData(organized)
51
+ setSelectedChart(null)
52
+ setFileName(name)
53
+ } catch (error) {
54
+ console.error("Failed to parse JSONL:", error)
55
+ toast({
56
+ variant: "destructive",
57
+ title: "Parse error",
58
+ description: "Failed to parse JSONL file. Please check the file format.",
59
+ })
60
+ }
61
+ }
62
+
63
+ const handleReload = async () => {
64
+ if (!isAutoLoaded) return
65
+
66
+ setIsLoading(true)
67
+ try {
68
+ const response = await fetch('/api/jsonl')
69
+ if (response.ok) {
70
+ const { content, filename } = await response.json()
71
+ handleFileLoad(content, filename)
72
+ toast({
73
+ title: "Reloaded",
74
+ description: `Reloaded ${filename} from filesystem`,
75
+ })
76
+ } else {
77
+ throw new Error('Failed to reload')
78
+ }
79
+ } catch (error) {
80
+ toast({
81
+ variant: "destructive",
82
+ title: "Reload failed",
83
+ description: "Failed to reload file from filesystem",
84
+ })
85
+ } finally {
86
+ setIsLoading(false)
87
+ }
88
+ }
89
+
90
+ const handleSave = async () => {
91
+ if (!data || !isAutoLoaded) return
92
+
93
+ setIsLoading(true)
94
+ try {
95
+ const jsonlLines: string[] = []
96
+
97
+ // Export all entities
98
+ for (const entity of data.entities.values()) {
99
+ jsonlLines.push(JSON.stringify(entity))
100
+ }
101
+
102
+ // Export all relations
103
+ for (const relation of data.relations) {
104
+ jsonlLines.push(JSON.stringify(relation))
105
+ }
106
+
107
+ const response = await fetch('/api/jsonl', {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ content: jsonlLines.join('\n') + '\n' })
111
+ })
112
+
113
+ if (response.ok) {
114
+ const { backup } = await response.json()
115
+ toast({
116
+ title: "Saved",
117
+ description: `File saved. Backup created at ${backup}`,
118
+ })
119
+ } else {
120
+ throw new Error('Save failed')
121
+ }
122
+ } catch (error) {
123
+ toast({
124
+ variant: "destructive",
125
+ title: "Save failed",
126
+ description: "Failed to save changes to filesystem",
127
+ })
128
+ } finally {
129
+ setIsLoading(false)
130
+ }
131
+ }
132
+
133
+ const handleExportData = () => {
134
+ if (!data) return
135
+
136
+ const jsonlLines: string[] = []
137
+
138
+ // Export all entities
139
+ for (const entity of data.entities.values()) {
140
+ jsonlLines.push(JSON.stringify(entity))
141
+ }
142
+
143
+ // Export all relations
144
+ for (const relation of data.relations) {
145
+ jsonlLines.push(JSON.stringify(relation))
146
+ }
147
+
148
+ const blob = new Blob([jsonlLines.join("\n") + "\n"], { type: "application/jsonl" })
149
+ const url = URL.createObjectURL(blob)
150
+ const a = document.createElement("a")
151
+ a.href = url
152
+ a.download = fileName || "coaia-narrative-export.jsonl"
153
+ document.body.appendChild(a)
154
+ a.click()
155
+ document.body.removeChild(a)
156
+ URL.revokeObjectURL(url)
157
+ }
158
+
159
+ return (
160
+ <div className="min-h-screen bg-background">
161
+ <header className="border-b border-border bg-card">
162
+ <div className="container mx-auto px-4 py-6">
163
+ <div className="flex items-center justify-between">
164
+ <div>
165
+ <h1 className="text-3xl font-bold text-balance">COAIA Narrative Visualizer</h1>
166
+ <p className="text-muted-foreground mt-2">
167
+ Visualize and explore structural tension charts from coaia-narrative JSONL files
168
+ </p>
169
+ {isAutoLoaded && (
170
+ <p className="text-xs text-muted-foreground mt-1">
171
+ 📁 {fileName}
172
+ </p>
173
+ )}
174
+ </div>
175
+ {data && (
176
+ <div className="flex items-center gap-2">
177
+ {isAutoLoaded && (
178
+ <>
179
+ <Button onClick={handleSave} variant="default" size="sm" disabled={isLoading}>
180
+ <Save className="w-4 h-4 mr-2" />
181
+ Save Changes
182
+ </Button>
183
+ <Button onClick={handleReload} variant="outline" size="sm" disabled={isLoading}>
184
+ <RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
185
+ Reload
186
+ </Button>
187
+ </>
188
+ )}
189
+ <Button onClick={handleExportData} variant="outline" size="sm">
190
+ <Download className="w-4 h-4 mr-2" />
191
+ Export JSONL
192
+ </Button>
193
+ <Button onClick={() => setData(null)} variant="outline" size="sm">
194
+ <Upload className="w-4 h-4 mr-2" />
195
+ Load New File
196
+ </Button>
197
+ </div>
198
+ )}
199
+ </div>
200
+ </div>
201
+ </header>
202
+
203
+ <main className="container mx-auto px-4 py-8">
204
+ {!data ? (
205
+ <div className="max-w-2xl mx-auto">
206
+ <FileUpload onFileLoad={handleFileLoad} />
207
+ <div className="mt-8 p-6 bg-muted/30 rounded-lg">
208
+ <h2 className="text-lg font-semibold mb-3">About This Tool</h2>
209
+ <p className="text-sm text-muted-foreground leading-relaxed mb-4">
210
+ This visualizer helps you explore and understand structural tension charts created by the
211
+ coaia-narrative MCP server. It displays the creative tension between current reality and desired
212
+ outcomes, action steps, narrative beats across multiple universes, and the relationships between
213
+ entities.
214
+ </p>
215
+ <div className="space-y-2">
216
+ <h3 className="text-sm font-semibold">Supported Features:</h3>
217
+ <ul className="text-sm text-muted-foreground space-y-1">
218
+ <li className="flex items-start gap-2">
219
+ <span className="text-chart-1 mt-1">•</span>
220
+ <span>Hierarchical chart visualization with expandable sub-charts</span>
221
+ </li>
222
+ <li className="flex items-start gap-2">
223
+ <span className="text-chart-2 mt-1">•</span>
224
+ <span>Action step tracking with completion status and due dates</span>
225
+ </li>
226
+ <li className="flex items-start gap-2">
227
+ <span className="text-chart-3 mt-1">•</span>
228
+ <span>Narrative beats with multi-universe perspectives</span>
229
+ </li>
230
+ <li className="flex items-start gap-2">
231
+ <span className="text-chart-4 mt-1">•</span>
232
+ <span>Entity relation graph visualization</span>
233
+ </li>
234
+ <li className="flex items-start gap-2">
235
+ <span className="text-chart-5 mt-1">•</span>
236
+ <span>Local file editing and auto-saving when launched via CLI</span>
237
+ </li>
238
+ </ul>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ ) : (
243
+ <div className="space-y-6">
244
+ <DataStats data={data} />
245
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
246
+ <div className="lg:col-span-1">
247
+ <Tabs value={viewMode} onValueChange={(v) => setViewMode(v as any)}>
248
+ <TabsList className="w-full">
249
+ <TabsTrigger value="hierarchy" className="flex-1">
250
+ Hierarchy
251
+ </TabsTrigger>
252
+ <TabsTrigger value="list" className="flex-1">
253
+ All Charts
254
+ </TabsTrigger>
255
+ </TabsList>
256
+ <TabsContent value="hierarchy" className="mt-4">
257
+ <ChartList
258
+ data={data}
259
+ selectedChart={selectedChart}
260
+ onSelectChart={setSelectedChart}
261
+ mode="hierarchy"
262
+ />
263
+ </TabsContent>
264
+ <TabsContent value="list" className="mt-4">
265
+ <ChartList data={data} selectedChart={selectedChart} onSelectChart={setSelectedChart} mode="list" />
266
+ </TabsContent>
267
+ </Tabs>
268
+ </div>
269
+ <div className="lg:col-span-2">
270
+ {selectedChart ? (
271
+ <ChartDetail chart={selectedChart} data={data} />
272
+ ) : (
273
+ <div className="bg-card border border-border rounded-lg p-12 text-center">
274
+ <p className="text-muted-foreground">Select a chart from the list to view details</p>
275
+ </div>
276
+ )}
277
+ </div>
278
+ </div>
279
+ </div>
280
+ )}
281
+ </main>
282
+ </div>
283
+ )
284
+ }
package/cli.ts ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * COAIA Visualizer CLI - Launch visualizer with local memory file
5
+ *
6
+ * Launches Next.js dev server with pre-loaded memory file
7
+ * Works analogously to coaia-narrative/cli.ts
8
+ */
9
+
10
+ import { spawn } from 'child_process';
11
+ import { promises as fs } from 'fs';
12
+ import path from 'path';
13
+ import minimist from 'minimist';
14
+ import * as dotenv from 'dotenv';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+
20
+ interface Config {
21
+ memoryPath: string;
22
+ port: number;
23
+ noOpen: boolean;
24
+ }
25
+
26
+ function loadConfig(args: minimist.ParsedArgs): Config {
27
+ let config: Config = {
28
+ memoryPath: path.join(process.cwd(), 'memory.jsonl'),
29
+ port: 3000,
30
+ noOpen: false
31
+ };
32
+
33
+ // Load .env files
34
+ const localEnvPath = path.join(process.cwd(), '.env');
35
+ try {
36
+ dotenv.config({ path: localEnvPath });
37
+ } catch (error) {
38
+ // .env doesn't exist, that's okay
39
+ }
40
+
41
+ // Custom env file
42
+ if (args.env) {
43
+ dotenv.config({ path: args.env, override: true });
44
+ }
45
+
46
+ // Environment variables
47
+ if (process.env.COAIAN_MF) {
48
+ config.memoryPath = process.env.COAIAN_MF;
49
+ }
50
+ if (process.env.COAIAV_PORT) {
51
+ config.port = parseInt(process.env.COAIAV_PORT, 10);
52
+ }
53
+
54
+ // Command-line flags override everything
55
+ if (args['memory-path'] || args['M']) {
56
+ config.memoryPath = args['memory-path'] || args['M'];
57
+ }
58
+ if (args['port'] || args['p']) {
59
+ config.port = args['port'] || args['p'];
60
+ }
61
+ if (args['no-open']) {
62
+ config.noOpen = true;
63
+ }
64
+
65
+ return config;
66
+ }
67
+
68
+ async function main() {
69
+ const args = minimist(process.argv.slice(2));
70
+
71
+ if (args.help || args.h) {
72
+ console.log(`
73
+ 🎨 COAIA Visualizer - Interactive Chart Viewer
74
+
75
+ DESCRIPTION:
76
+ Web-based visualizer for structural tension charts from coaia-narrative.
77
+ Launches a local web server to interactively explore your charts.
78
+
79
+ USAGE:
80
+ coaia-visualizer [OPTIONS]
81
+ npx coaia-visualizer [OPTIONS]
82
+
83
+ OPTIONS:
84
+ --memory-path PATH, -M PATH Path to memory.jsonl file (default: ./memory.jsonl)
85
+ --port PORT, -p PORT Server port (default: 3000)
86
+ --no-open Don't auto-open browser
87
+ --help, -h Show this help message
88
+
89
+ ENVIRONMENT VARIABLES:
90
+ COAIAN_MF Default memory file path
91
+ COAIAV_PORT Default server port
92
+
93
+ EXAMPLES:
94
+ # Launch with default memory.jsonl
95
+ coaia-visualizer
96
+
97
+ # Launch with specific memory file
98
+ coaia-visualizer --memory-path ./my-charts.jsonl
99
+
100
+ # Launch on different port
101
+ coaia-visualizer --port 3001
102
+
103
+ # Use environment variable
104
+ COAIAN_MF=./charts.jsonl coaia-visualizer
105
+ `);
106
+ process.exit(0);
107
+ }
108
+
109
+ const config = loadConfig(args);
110
+
111
+ // Verify memory file exists
112
+ try {
113
+ await fs.access(config.memoryPath);
114
+ } catch (error) {
115
+ console.error(`❌ Memory file not found: ${config.memoryPath}`);
116
+ console.error(` Create a memory file or specify a different path with --memory-path`);
117
+ process.exit(1);
118
+ }
119
+
120
+ console.log(`🎨 COAIA Visualizer`);
121
+ console.log(`📁 Memory file: ${config.memoryPath}`);
122
+ console.log(`🌐 Port: ${config.port}`);
123
+ console.log();
124
+
125
+ // Set environment variables for Next.js
126
+ process.env.COAIAV_MEMORY_PATH = path.resolve(config.memoryPath);
127
+ process.env.PORT = config.port.toString();
128
+
129
+ // Navigate to visualizer root
130
+ const visualizerRoot = path.resolve(__dirname, '..');
131
+
132
+ // Launch Next.js dev server with explicit port flag
133
+ const nextProcess = spawn('npm', ['run', 'dev', '--', '-p', config.port.toString()], {
134
+ cwd: visualizerRoot,
135
+ stdio: 'inherit',
136
+ env: {
137
+ ...process.env,
138
+ COAIAV_MEMORY_PATH: path.resolve(config.memoryPath),
139
+ PORT: config.port.toString()
140
+ }
141
+ });
142
+
143
+ // Open browser if not disabled
144
+ if (!config.noOpen) {
145
+ setTimeout(() => {
146
+ const url = `http://localhost:${config.port}`;
147
+ console.log(`\n🚀 Opening browser: ${url}\n`);
148
+
149
+ const openCmd = process.platform === 'darwin' ? 'open' :
150
+ process.platform === 'win32' ? 'start' : 'xdg-open';
151
+ spawn(openCmd, [url], { detached: true, stdio: 'ignore' }).unref();
152
+ }, 3000);
153
+ }
154
+
155
+ // Handle shutdown
156
+ process.on('SIGINT', () => {
157
+ console.log('\n👋 Shutting down visualizer...');
158
+ nextProcess.kill();
159
+ process.exit(0);
160
+ });
161
+
162
+ nextProcess.on('exit', (code) => {
163
+ process.exit(code || 0);
164
+ });
165
+ }
166
+
167
+ main().catch((error) => {
168
+ console.error('❌ Fatal error:', error);
169
+ process.exit(1);
170
+ });
@@ -0,0 +1,213 @@
1
+ "use client"
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import { Separator } from "@/components/ui/separator"
6
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
7
+ import type { ParsedData, Chart, EntityRecord } from "@/lib/types"
8
+ import { Target, MapPin, ListChecks, Network, BookOpen, Calendar, TrendingUp, CheckCircle2, Circle } from "lucide-react"
9
+ import { NarrativeBeats } from "./narrative-beats"
10
+ import { RelationGraph } from "./relation-graph"
11
+
12
+ interface ChartDetailProps {
13
+ chart: Chart
14
+ data: ParsedData
15
+ }
16
+
17
+ export function ChartDetail({ chart, data }: ChartDetailProps) {
18
+ const dueDate = chart.chartEntity.metadata.dueDate
19
+ ? new Date(chart.chartEntity.metadata.dueDate).toLocaleDateString()
20
+ : null
21
+ const createdDate = new Date(chart.chartEntity.metadata.createdAt).toLocaleDateString()
22
+
23
+ return (
24
+ <div className="space-y-6">
25
+ {/* Header Card */}
26
+ <Card>
27
+ <CardHeader>
28
+ <div className="flex items-start justify-between gap-4">
29
+ <div className="flex-1">
30
+ <div className="flex items-center gap-2 mb-2">
31
+ <CardTitle className="text-2xl">Chart {chart.id}</CardTitle>
32
+ <Badge>Level {chart.level}</Badge>
33
+ {chart.parentChart && <Badge variant="outline">Sub-chart</Badge>}
34
+ </div>
35
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
36
+ <span>Created {createdDate}</span>
37
+ {dueDate && (
38
+ <div className="flex items-center gap-1">
39
+ <Calendar className="w-4 h-4" />
40
+ <span>Due {dueDate}</span>
41
+ </div>
42
+ )}
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </CardHeader>
47
+ </Card>
48
+
49
+ {/* Structural Tension */}
50
+ <Card>
51
+ <CardHeader>
52
+ <CardTitle className="flex items-center gap-2">
53
+ <TrendingUp className="w-5 h-5" />
54
+ Structural Tension
55
+ </CardTitle>
56
+ <CardDescription>The creative tension between current reality and desired outcome</CardDescription>
57
+ </CardHeader>
58
+ <CardContent className="space-y-6">
59
+ {/* Desired Outcome */}
60
+ {chart.desiredOutcome && (
61
+ <div>
62
+ <div className="flex items-center gap-2 mb-3">
63
+ <Target className="w-5 h-5 text-chart-1" />
64
+ <h3 className="font-semibold">Desired Outcome</h3>
65
+ </div>
66
+ <div className="bg-chart-1/10 border border-chart-1/20 rounded-lg p-4">
67
+ {chart.desiredOutcome.observations.map((obs, idx) => (
68
+ <p key={idx} className="text-sm leading-relaxed">
69
+ {obs}
70
+ </p>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ )}
75
+
76
+ <Separator />
77
+
78
+ {/* Current Reality */}
79
+ {chart.currentReality && (
80
+ <div>
81
+ <div className="flex items-center gap-2 mb-3">
82
+ <MapPin className="w-5 h-5 text-chart-2" />
83
+ <h3 className="font-semibold">Current Reality</h3>
84
+ </div>
85
+ <div className="bg-chart-2/10 border border-chart-2/20 rounded-lg p-4">
86
+ {chart.currentReality.observations.map((obs, idx) => (
87
+ <p key={idx} className="text-sm leading-relaxed">
88
+ {obs}
89
+ </p>
90
+ ))}
91
+ </div>
92
+ </div>
93
+ )}
94
+ </CardContent>
95
+ </Card>
96
+
97
+ {/* Narrative Beats */}
98
+ {chart.narrativeBeats.length > 0 && (
99
+ <div>
100
+ <div className="flex items-center gap-2 mb-4">
101
+ <BookOpen className="w-5 h-5 text-chart-3" />
102
+ <h2 className="text-xl font-semibold">Narrative Beats</h2>
103
+ <Badge className="bg-chart-3 text-white">{chart.narrativeBeats.length}</Badge>
104
+ </div>
105
+ <NarrativeBeats beats={chart.narrativeBeats} />
106
+ </div>
107
+ )}
108
+
109
+ {/* Tabs for Actions and Relations */}
110
+ <Tabs defaultValue="actions" className="w-full">
111
+ <TabsList className="grid w-full grid-cols-2">
112
+ <TabsTrigger value="actions">
113
+ <ListChecks className="w-4 h-4 mr-2" />
114
+ Actions ({chart.actions.length})
115
+ </TabsTrigger>
116
+ <TabsTrigger value="relations">
117
+ <Network className="w-4 h-4 mr-2" />
118
+ Relations ({chart.relations.length})
119
+ </TabsTrigger>
120
+ </TabsList>
121
+
122
+ <TabsContent value="actions" className="mt-4">
123
+ <Card>
124
+ <CardHeader>
125
+ <CardTitle>Action Steps</CardTitle>
126
+ <CardDescription>Strategic actions advancing toward the desired outcome</CardDescription>
127
+ </CardHeader>
128
+ <CardContent>
129
+ {chart.actions.length === 0 ? (
130
+ <p className="text-sm text-muted-foreground text-center py-8">No action steps defined yet</p>
131
+ ) : (
132
+ <div className="space-y-3">
133
+ {chart.actions.map((action, idx) => (
134
+ <ActionItem key={action.name} action={action} index={idx + 1} />
135
+ ))}
136
+ </div>
137
+ )}
138
+ </CardContent>
139
+ </Card>
140
+ </TabsContent>
141
+
142
+ <TabsContent value="relations" className="mt-4">
143
+ <RelationGraph chart={chart} data={data} />
144
+ </TabsContent>
145
+ </Tabs>
146
+
147
+ {/* Sub-charts */}
148
+ {chart.subCharts.length > 0 && (
149
+ <Card>
150
+ <CardHeader>
151
+ <CardTitle>Sub-Charts ({chart.subCharts.length})</CardTitle>
152
+ <CardDescription>Telescoped action steps broken into detailed sub-charts</CardDescription>
153
+ </CardHeader>
154
+ <CardContent>
155
+ <div className="grid gap-3">
156
+ {chart.subCharts.map((subChart) => (
157
+ <div key={subChart.id} className="border border-border rounded-lg p-3">
158
+ <div className="flex items-center gap-2 mb-2">
159
+ <span className="text-sm font-mono text-muted-foreground">chart_{subChart.id}</span>
160
+ <Badge variant="secondary" className="text-xs">
161
+ Level {subChart.level}
162
+ </Badge>
163
+ </div>
164
+ {subChart.desiredOutcome && (
165
+ <p className="text-sm line-clamp-2">{subChart.desiredOutcome.observations[0]}</p>
166
+ )}
167
+ </div>
168
+ ))}
169
+ </div>
170
+ </CardContent>
171
+ </Card>
172
+ )}
173
+ </div>
174
+ )
175
+ }
176
+
177
+ interface ActionItemProps {
178
+ action: EntityRecord
179
+ index: number
180
+ }
181
+
182
+ function ActionItem({ action, index }: ActionItemProps) {
183
+ const isComplete = action.metadata.completionStatus === true
184
+ const dueDate = action.metadata.dueDate ? new Date(action.metadata.dueDate).toLocaleDateString() : null
185
+
186
+ return (
187
+ <div
188
+ className={`flex items-start gap-3 p-3 rounded-lg border ${
189
+ isComplete ? "bg-muted/50 border-muted" : "bg-card border-border"
190
+ }`}
191
+ >
192
+ <div className="flex-shrink-0 mt-0.5">
193
+ {isComplete ? (
194
+ <CheckCircle2 className="w-5 h-5 text-chart-4" />
195
+ ) : (
196
+ <Circle className="w-5 h-5 text-muted-foreground" />
197
+ )}
198
+ </div>
199
+ <div className="flex-1 min-w-0">
200
+ <div className="flex items-start justify-between gap-2 mb-1">
201
+ <span className="text-sm font-medium">Action {index}</span>
202
+ {dueDate && (
203
+ <div className="flex items-center gap-1 text-xs text-muted-foreground">
204
+ <Calendar className="w-3 h-3" />
205
+ <span>{dueDate}</span>
206
+ </div>
207
+ )}
208
+ </div>
209
+ <p className={`text-sm ${isComplete ? "text-muted-foreground line-through" : ""}`}>{action.observations[0]}</p>
210
+ </div>
211
+ </div>
212
+ )
213
+ }