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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { toast as sonnerToast } from "sonner"
|
|
2
|
+
|
|
3
|
+
export function useToast() {
|
|
4
|
+
return {
|
|
5
|
+
toast: ({ title, description, variant }: {
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
variant?: "default" | "destructive"
|
|
9
|
+
}) => {
|
|
10
|
+
if (variant === "destructive") {
|
|
11
|
+
sonnerToast.error(title || "Error", { description })
|
|
12
|
+
} else {
|
|
13
|
+
sonnerToast.success(title || "Success", { description })
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { EntityRecord, RelationRecord, JSONLRecord, Chart, ParsedData } from "./types"
|
|
2
|
+
|
|
3
|
+
export function parseJSONL(content: string): JSONLRecord[] {
|
|
4
|
+
const lines = content.trim().split("\n")
|
|
5
|
+
const records: JSONLRecord[] = []
|
|
6
|
+
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
if (!line.trim()) continue
|
|
9
|
+
try {
|
|
10
|
+
const record = JSON.parse(line)
|
|
11
|
+
records.push(record)
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error("Failed to parse line:", line, error)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return records
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function organizeData(records: JSONLRecord[]): ParsedData {
|
|
21
|
+
const entities = new Map<string, EntityRecord>()
|
|
22
|
+
const relations: RelationRecord[] = []
|
|
23
|
+
const charts = new Map<string, Chart>()
|
|
24
|
+
|
|
25
|
+
console.log("[v0] Total records to process:", records.length)
|
|
26
|
+
|
|
27
|
+
// First pass: separate entities and relations
|
|
28
|
+
for (const record of records) {
|
|
29
|
+
if (record.type === "entity") {
|
|
30
|
+
entities.set(record.name, record)
|
|
31
|
+
} else if (record.type === "relation") {
|
|
32
|
+
relations.push(record)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log("[v0] Total entities:", entities.size)
|
|
37
|
+
console.log("[v0] Total relations:", relations.length)
|
|
38
|
+
|
|
39
|
+
// Second pass: create chart structures
|
|
40
|
+
for (const [name, entity] of entities.entries()) {
|
|
41
|
+
if (entity.entityType === "structural_tension_chart") {
|
|
42
|
+
const chartId = entity.metadata.chartId || entity.name.replace("_chart", "")
|
|
43
|
+
console.log("[v0] Found chart:", chartId, "Level:", entity.metadata.level, "Name:", entity.name)
|
|
44
|
+
|
|
45
|
+
if (!charts.has(chartId)) {
|
|
46
|
+
charts.set(chartId, {
|
|
47
|
+
id: chartId,
|
|
48
|
+
chartEntity: entity,
|
|
49
|
+
actions: [],
|
|
50
|
+
narrativeBeats: [],
|
|
51
|
+
subCharts: [],
|
|
52
|
+
level: entity.metadata.level || 0,
|
|
53
|
+
parentChart: entity.metadata.parentChart,
|
|
54
|
+
relations: [],
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log("[v0] Total charts created:", charts.size)
|
|
61
|
+
|
|
62
|
+
// Third pass: populate charts with their components
|
|
63
|
+
for (const [name, entity] of entities.entries()) {
|
|
64
|
+
let chartId = entity.metadata.chartId
|
|
65
|
+
|
|
66
|
+
// If no chartId in metadata, extract from entity name
|
|
67
|
+
if (!chartId && name.includes("_")) {
|
|
68
|
+
const match = name.match(/^(chart_\d+)/)
|
|
69
|
+
if (match) {
|
|
70
|
+
chartId = match[1]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const chart = charts.get(chartId)
|
|
75
|
+
|
|
76
|
+
if (!chart) {
|
|
77
|
+
if (entity.entityType !== "structural_tension_chart") {
|
|
78
|
+
console.log("[v0] No chart found for entity:", name, "chartId:", chartId)
|
|
79
|
+
}
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (entity.entityType) {
|
|
84
|
+
case "desired_outcome":
|
|
85
|
+
chart.desiredOutcome = entity
|
|
86
|
+
break
|
|
87
|
+
case "current_reality":
|
|
88
|
+
chart.currentReality = entity
|
|
89
|
+
break
|
|
90
|
+
case "action_step":
|
|
91
|
+
chart.actions.push(entity)
|
|
92
|
+
break
|
|
93
|
+
case "narrative_beat":
|
|
94
|
+
chart.narrativeBeats.push(entity)
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const [id, chart] of charts.entries()) {
|
|
100
|
+
console.log(
|
|
101
|
+
`[v0] Chart ${id}: ${chart.actions.length} actions, ${chart.narrativeBeats.length} beats, level ${chart.level}`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fourth pass: organize chart hierarchy and relations
|
|
106
|
+
for (const chart of charts.values()) {
|
|
107
|
+
chart.relations = relations.filter(
|
|
108
|
+
(r) =>
|
|
109
|
+
r.from.startsWith(chart.id) ||
|
|
110
|
+
r.to.startsWith(chart.id) ||
|
|
111
|
+
r.from.startsWith(`chart_${chart.id}`) ||
|
|
112
|
+
r.to.startsWith(`chart_${chart.id}`),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if (chart.parentChart) {
|
|
116
|
+
const parentId = chart.parentChart.replace("chart_", "")
|
|
117
|
+
const parentChart = charts.get(parentId) || charts.get(chart.parentChart)
|
|
118
|
+
if (parentChart) {
|
|
119
|
+
parentChart.subCharts.push(chart)
|
|
120
|
+
console.log(`[v0] Added ${chart.id} as subChart of ${parentChart.id}`)
|
|
121
|
+
} else {
|
|
122
|
+
console.log(`[v0] Parent chart not found: ${chart.parentChart} for chart ${chart.id}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Identify root charts (level 0 or no parent)
|
|
128
|
+
const rootCharts = Array.from(charts.values()).filter((chart) => chart.level === 0 || !chart.parentChart)
|
|
129
|
+
|
|
130
|
+
console.log("[v0] Root charts found:", rootCharts.length)
|
|
131
|
+
console.log(
|
|
132
|
+
"[v0] Root chart IDs:",
|
|
133
|
+
rootCharts.map((c) => c.id),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
charts: Array.from(charts.values()),
|
|
138
|
+
entities,
|
|
139
|
+
relations,
|
|
140
|
+
rootCharts,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getChartSummary(chart: Chart): string {
|
|
145
|
+
const outcome = chart.desiredOutcome?.observations[0] || "No desired outcome"
|
|
146
|
+
return outcome.length > 100 ? outcome.substring(0, 100) + "..." : outcome
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getChartProgress(chart: Chart): number {
|
|
150
|
+
if (chart.actions.length === 0) return 0
|
|
151
|
+
const completed = chart.actions.filter((a) => a.metadata.completionStatus).length
|
|
152
|
+
return Math.round((completed / chart.actions.length) * 100)
|
|
153
|
+
}
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Type definitions for coaia-narrative JSONL data structures
|
|
2
|
+
|
|
3
|
+
export interface EntityRecord {
|
|
4
|
+
type: "entity"
|
|
5
|
+
name: string
|
|
6
|
+
entityType: "structural_tension_chart" | "desired_outcome" | "current_reality" | "action_step" | "narrative_beat"
|
|
7
|
+
observations: string[]
|
|
8
|
+
metadata: Record<string, any>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RelationRecord {
|
|
12
|
+
type: "relation"
|
|
13
|
+
from: string
|
|
14
|
+
to: string
|
|
15
|
+
relationType: "contains" | "creates_tension_with" | "advances_toward" | "documents"
|
|
16
|
+
metadata: Record<string, any>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type JSONLRecord = EntityRecord | RelationRecord
|
|
20
|
+
|
|
21
|
+
export interface Chart {
|
|
22
|
+
id: string
|
|
23
|
+
chartEntity: EntityRecord
|
|
24
|
+
desiredOutcome?: EntityRecord
|
|
25
|
+
currentReality?: EntityRecord
|
|
26
|
+
actions: EntityRecord[]
|
|
27
|
+
narrativeBeats: EntityRecord[]
|
|
28
|
+
subCharts: Chart[]
|
|
29
|
+
level: number
|
|
30
|
+
parentChart?: string
|
|
31
|
+
relations: RelationRecord[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ParsedData {
|
|
35
|
+
charts: Chart[]
|
|
36
|
+
entities: Map<string, EntityRecord>
|
|
37
|
+
relations: RelationRecord[]
|
|
38
|
+
rootCharts: Chart[]
|
|
39
|
+
}
|
package/lib/utils.ts
ADDED
package/next.config.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coaia-visualizer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coaia-visualizer": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "next build",
|
|
11
|
+
"dev": "next dev",
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"start": "next start",
|
|
14
|
+
"build:cli": "tsc cli.ts --outDir dist --module esnext --target es2022 --moduleResolution bundler --esModuleInterop --skipLibCheck",
|
|
15
|
+
"prepare": "npm run build:cli"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@hookform/resolvers": "^3.10.0",
|
|
19
|
+
"@radix-ui/react-accordion": "1.2.2",
|
|
20
|
+
"@radix-ui/react-alert-dialog": "1.1.4",
|
|
21
|
+
"@radix-ui/react-aspect-ratio": "1.1.1",
|
|
22
|
+
"@radix-ui/react-avatar": "1.1.2",
|
|
23
|
+
"@radix-ui/react-checkbox": "1.1.3",
|
|
24
|
+
"@radix-ui/react-collapsible": "1.1.2",
|
|
25
|
+
"@radix-ui/react-context-menu": "2.2.4",
|
|
26
|
+
"@radix-ui/react-dialog": "1.1.4",
|
|
27
|
+
"@radix-ui/react-dropdown-menu": "2.1.4",
|
|
28
|
+
"@radix-ui/react-hover-card": "1.1.4",
|
|
29
|
+
"@radix-ui/react-label": "2.1.1",
|
|
30
|
+
"@radix-ui/react-menubar": "1.1.4",
|
|
31
|
+
"@radix-ui/react-navigation-menu": "1.2.3",
|
|
32
|
+
"@radix-ui/react-popover": "1.1.4",
|
|
33
|
+
"@radix-ui/react-progress": "1.1.1",
|
|
34
|
+
"@radix-ui/react-radio-group": "1.2.2",
|
|
35
|
+
"@radix-ui/react-scroll-area": "1.2.2",
|
|
36
|
+
"@radix-ui/react-select": "2.1.4",
|
|
37
|
+
"@radix-ui/react-separator": "1.1.1",
|
|
38
|
+
"@radix-ui/react-slider": "1.2.2",
|
|
39
|
+
"@radix-ui/react-slot": "1.1.1",
|
|
40
|
+
"@radix-ui/react-switch": "1.1.2",
|
|
41
|
+
"@radix-ui/react-tabs": "1.1.2",
|
|
42
|
+
"@radix-ui/react-toast": "1.2.4",
|
|
43
|
+
"@radix-ui/react-toggle": "1.1.1",
|
|
44
|
+
"@radix-ui/react-toggle-group": "1.1.1",
|
|
45
|
+
"@radix-ui/react-tooltip": "1.1.6",
|
|
46
|
+
"@types/minimist": "^1.2.5",
|
|
47
|
+
"@vercel/analytics": "1.3.1",
|
|
48
|
+
"autoprefixer": "^10.4.20",
|
|
49
|
+
"class-variance-authority": "^0.7.1",
|
|
50
|
+
"clsx": "^2.1.1",
|
|
51
|
+
"cmdk": "1.0.4",
|
|
52
|
+
"date-fns": "4.1.0",
|
|
53
|
+
"dotenv": "^17.2.3",
|
|
54
|
+
"embla-carousel-react": "8.5.1",
|
|
55
|
+
"input-otp": "1.4.1",
|
|
56
|
+
"lucide-react": "^0.454.0",
|
|
57
|
+
"minimist": "^1.2.8",
|
|
58
|
+
"next": "16.0.10",
|
|
59
|
+
"next-themes": "^0.4.6",
|
|
60
|
+
"react": "19.2.0",
|
|
61
|
+
"react-day-picker": "9.8.0",
|
|
62
|
+
"react-dom": "19.2.0",
|
|
63
|
+
"react-hook-form": "^7.60.0",
|
|
64
|
+
"react-resizable-panels": "^2.1.7",
|
|
65
|
+
"recharts": "2.15.4",
|
|
66
|
+
"sonner": "^1.7.4",
|
|
67
|
+
"tailwind-merge": "^3.3.1",
|
|
68
|
+
"tailwindcss-animate": "^1.0.7",
|
|
69
|
+
"vaul": "^1.1.2",
|
|
70
|
+
"zod": "3.25.76"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@tailwindcss/postcss": "^4.1.9",
|
|
74
|
+
"@types/node": "^22",
|
|
75
|
+
"@types/react": "^19",
|
|
76
|
+
"@types/react-dom": "^19",
|
|
77
|
+
"postcss": "^8.5",
|
|
78
|
+
"tailwindcss": "^4.1.9",
|
|
79
|
+
"tw-animate-css": "1.3.3",
|
|
80
|
+
"typescript": "^5"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/public/icon.svg
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<style>
|
|
3
|
+
@media (prefers-color-scheme: light) {
|
|
4
|
+
.background { fill: black; }
|
|
5
|
+
.foreground { fill: white; }
|
|
6
|
+
}
|
|
7
|
+
@media (prefers-color-scheme: dark) {
|
|
8
|
+
.background { fill: white; }
|
|
9
|
+
.foreground { fill: black; }
|
|
10
|
+
}
|
|
11
|
+
</style>
|
|
12
|
+
<g clip-path="url(#clip0_7960_43945)">
|
|
13
|
+
<rect class="background" width="180" height="180" rx="37" />
|
|
14
|
+
<g style="transform: scale(95%); transform-origin: center">
|
|
15
|
+
<path class="foreground"
|
|
16
|
+
d="M101.141 53H136.632C151.023 53 162.689 64.6662 162.689 79.0573V112.904H148.112V79.0573C148.112 78.7105 148.098 78.3662 148.072 78.0251L112.581 112.898C112.701 112.902 112.821 112.904 112.941 112.904H148.112V126.672H112.941C98.5504 126.672 86.5638 114.891 86.5638 100.5V66.7434H101.141V100.5C101.141 101.15 101.191 101.792 101.289 102.422L137.56 66.7816C137.255 66.7563 136.945 66.7434 136.632 66.7434H101.141V53Z" />
|
|
17
|
+
<path class="foreground"
|
|
18
|
+
d="M65.2926 124.136L14 66.7372H34.6355L64.7495 100.436V66.7372H80.1365V118.47C80.1365 126.278 70.4953 129.958 65.2926 124.136Z" />
|
|
19
|
+
</g>
|
|
20
|
+
</g>
|
|
21
|
+
<defs>
|
|
22
|
+
<clipPath id="clip0_7960_43945">
|
|
23
|
+
<rect width="180" height="180" fill="white" />
|
|
24
|
+
</clipPath>
|
|
25
|
+
</defs>
|
|
26
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|