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.
- package/.hch/issues.json +156 -0
- package/.hch/issues.md +2 -0
- package/README.md +67 -0
- package/app/api/jsonl/route.ts +71 -0
- package/app/globals.css +125 -0
- package/app/layout.tsx +48 -0
- package/app/page.tsx +284 -0
- package/cli.ts +170 -0
- package/components/chart-detail.tsx +213 -0
- package/components/chart-list.tsx +184 -0
- package/components/data-stats.tsx +49 -0
- package/components/file-upload.tsx +73 -0
- package/components/narrative-beats.tsx +108 -0
- package/components/relation-graph.tsx +81 -0
- package/components/theme-provider.tsx +11 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/tabs.tsx +66 -0
- package/components.json +21 -0
- package/dist/cli.js +144 -0
- package/feat-2-webui-local-editing/IMPLEMENTATION.md +245 -0
- package/feat-2-webui-local-editing/INTEGRATION.md +302 -0
- package/feat-2-webui-local-editing/QUICKSTART.md +129 -0
- package/feat-2-webui-local-editing/README.md +254 -0
- package/feat-2-webui-local-editing/api-route-jsonl.ts +71 -0
- package/feat-2-webui-local-editing/cli.ts +170 -0
- package/feat-2-webui-local-editing/demo.sh +98 -0
- package/feat-2-webui-local-editing/package.json +82 -0
- package/feat-2-webui-local-editing/test-integration.sh +93 -0
- package/feat-2-webui-local-editing/updated-page.tsx +284 -0
- package/hooks/use-toast.ts +17 -0
- package/lib/jsonl-parser.ts +153 -0
- package/lib/types.ts +39 -0
- package/lib/utils.ts +6 -0
- package/next.config.mjs +12 -0
- package/package.json +82 -0
- package/postcss.config.mjs +8 -0
- package/public/apple-icon.png +0 -0
- package/public/icon-dark-32x32.png +0 -0
- package/public/icon-light-32x32.png +0 -0
- package/public/icon.svg +26 -0
- package/public/placeholder-logo.png +0 -0
- package/public/placeholder-logo.svg +1 -0
- package/public/placeholder-user.jpg +0 -0
- package/public/placeholder.jpg +0 -0
- package/public/placeholder.svg +1 -0
- package/styles/globals.css +125 -0
- 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
|
+
}
|