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,184 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
4
|
+
import { Badge } from "@/components/ui/badge"
|
|
5
|
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
6
|
+
import type { ParsedData, Chart } from "@/lib/types"
|
|
7
|
+
import { getChartSummary, getChartProgress } from "@/lib/jsonl-parser"
|
|
8
|
+
import { ChevronRight, ChevronDown, Target, Calendar, BookOpen } from "lucide-react"
|
|
9
|
+
import { useState } from "react"
|
|
10
|
+
import { cn } from "@/lib/utils"
|
|
11
|
+
|
|
12
|
+
interface ChartListProps {
|
|
13
|
+
data: ParsedData
|
|
14
|
+
selectedChart: Chart | null
|
|
15
|
+
onSelectChart: (chart: Chart) => void
|
|
16
|
+
mode: "hierarchy" | "list"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ChartList({ data, selectedChart, onSelectChart, mode }: ChartListProps) {
|
|
20
|
+
if (mode === "list") {
|
|
21
|
+
return (
|
|
22
|
+
<Card>
|
|
23
|
+
<CardHeader>
|
|
24
|
+
<CardTitle>All Charts</CardTitle>
|
|
25
|
+
<CardDescription>{data.charts.length} total charts</CardDescription>
|
|
26
|
+
</CardHeader>
|
|
27
|
+
<CardContent>
|
|
28
|
+
<ScrollArea className="h-[600px] pr-4">
|
|
29
|
+
<div className="space-y-2">
|
|
30
|
+
{data.charts.map((chart) => (
|
|
31
|
+
<ChartItem
|
|
32
|
+
key={chart.id}
|
|
33
|
+
chart={chart}
|
|
34
|
+
isSelected={selectedChart?.id === chart.id}
|
|
35
|
+
onSelect={onSelectChart}
|
|
36
|
+
showLevel
|
|
37
|
+
/>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
</ScrollArea>
|
|
41
|
+
</CardContent>
|
|
42
|
+
</Card>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Card>
|
|
48
|
+
<CardHeader>
|
|
49
|
+
<CardTitle>Chart Hierarchy</CardTitle>
|
|
50
|
+
<CardDescription>{data.rootCharts.length} root charts</CardDescription>
|
|
51
|
+
</CardHeader>
|
|
52
|
+
<CardContent>
|
|
53
|
+
<ScrollArea className="h-[600px] pr-4">
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
{data.rootCharts.map((chart) => (
|
|
56
|
+
<ChartTree key={chart.id} chart={chart} selectedChart={selectedChart} onSelectChart={onSelectChart} />
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</ScrollArea>
|
|
60
|
+
</CardContent>
|
|
61
|
+
</Card>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ChartTreeProps {
|
|
66
|
+
chart: Chart
|
|
67
|
+
selectedChart: Chart | null
|
|
68
|
+
onSelectChart: (chart: Chart) => void
|
|
69
|
+
level?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ChartTree({ chart, selectedChart, onSelectChart, level = 0 }: ChartTreeProps) {
|
|
73
|
+
const [isExpanded, setIsExpanded] = useState(true)
|
|
74
|
+
const hasSubCharts = chart.subCharts.length > 0
|
|
75
|
+
const isSelected = selectedChart?.id === chart.id
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className={cn(level > 0 && "ml-4 border-l border-border pl-4")}>
|
|
79
|
+
<div className="flex items-start gap-2">
|
|
80
|
+
{hasSubCharts && (
|
|
81
|
+
<button
|
|
82
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
83
|
+
className="mt-2 text-muted-foreground hover:text-foreground transition-colors"
|
|
84
|
+
>
|
|
85
|
+
{isExpanded ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
|
|
86
|
+
</button>
|
|
87
|
+
)}
|
|
88
|
+
{!hasSubCharts && <div className="w-4" />}
|
|
89
|
+
<div className="flex-1">
|
|
90
|
+
<ChartItem chart={chart} isSelected={isSelected} onSelect={onSelectChart} />
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
{isExpanded && hasSubCharts && (
|
|
94
|
+
<div className="mt-2 space-y-2">
|
|
95
|
+
{chart.subCharts.map((subChart) => (
|
|
96
|
+
<ChartTree
|
|
97
|
+
key={subChart.id}
|
|
98
|
+
chart={subChart}
|
|
99
|
+
selectedChart={selectedChart}
|
|
100
|
+
onSelectChart={onSelectChart}
|
|
101
|
+
level={level + 1}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface ChartItemProps {
|
|
111
|
+
chart: Chart
|
|
112
|
+
isSelected: boolean
|
|
113
|
+
onSelect: (chart: Chart) => void
|
|
114
|
+
showLevel?: boolean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ChartItem({ chart, isSelected, onSelect, showLevel }: ChartItemProps) {
|
|
118
|
+
const summary = getChartSummary(chart)
|
|
119
|
+
const progress = getChartProgress(chart)
|
|
120
|
+
const dueDate = chart.chartEntity.metadata.dueDate
|
|
121
|
+
? new Date(chart.chartEntity.metadata.dueDate).toLocaleDateString()
|
|
122
|
+
: null
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<button
|
|
126
|
+
onClick={() => onSelect(chart)}
|
|
127
|
+
className={cn(
|
|
128
|
+
"w-full text-left p-3 rounded-lg border transition-all",
|
|
129
|
+
isSelected ? "border-primary bg-primary/5" : "border-border hover:border-primary/50 bg-card",
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
<div className="flex items-start justify-between gap-2 mb-2">
|
|
133
|
+
<div className="flex items-center gap-2">
|
|
134
|
+
<Target className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
135
|
+
<span className="text-xs font-mono text-muted-foreground">chart_{chart.id}</span>
|
|
136
|
+
{showLevel && (
|
|
137
|
+
<Badge variant="outline" className="text-xs">
|
|
138
|
+
L{chart.level}
|
|
139
|
+
</Badge>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
{dueDate && (
|
|
143
|
+
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
144
|
+
<Calendar className="w-3 h-3" />
|
|
145
|
+
<span>{dueDate}</span>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
<p className="text-sm mb-2 line-clamp-2">{summary}</p>
|
|
150
|
+
|
|
151
|
+
{chart.narrativeBeats.length > 0 && (
|
|
152
|
+
<div className="mb-2 p-2 bg-chart-3/10 border border-chart-3/20 rounded">
|
|
153
|
+
<div className="flex items-center gap-2 mb-1">
|
|
154
|
+
<BookOpen className="w-3 h-3 text-chart-3" />
|
|
155
|
+
<span className="text-xs font-medium text-chart-3">
|
|
156
|
+
{chart.narrativeBeats.length} Narrative Beat{chart.narrativeBeats.length > 1 ? "s" : ""}
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
{chart.narrativeBeats[0]?.metadata?.narrative?.description && (
|
|
160
|
+
<p className="text-xs text-muted-foreground line-clamp-1 pl-5">
|
|
161
|
+
{chart.narrativeBeats[0].metadata.narrative.description}
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
<div className="flex items-center justify-between">
|
|
168
|
+
<div className="flex items-center gap-2">
|
|
169
|
+
<Badge variant="secondary" className="text-xs">
|
|
170
|
+
{chart.actions.length} actions
|
|
171
|
+
</Badge>
|
|
172
|
+
</div>
|
|
173
|
+
{chart.actions.length > 0 && (
|
|
174
|
+
<div className="flex items-center gap-2">
|
|
175
|
+
<div className="w-16 h-1.5 bg-secondary rounded-full overflow-hidden">
|
|
176
|
+
<div className="h-full bg-primary transition-all" style={{ width: `${progress}%` }} />
|
|
177
|
+
</div>
|
|
178
|
+
<span className="text-xs text-muted-foreground">{progress}%</span>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</button>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
4
|
+
import type { ParsedData } from "@/lib/types"
|
|
5
|
+
import { BarChart3, FileText, Network, Target } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
interface DataStatsProps {
|
|
8
|
+
data: ParsedData
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DataStats({ data }: DataStatsProps) {
|
|
12
|
+
const totalEntities = data.entities.size
|
|
13
|
+
const totalRelations = data.relations.length
|
|
14
|
+
const totalCharts = data.charts.length
|
|
15
|
+
const totalActions = data.charts.reduce((sum, chart) => sum + chart.actions.length, 0)
|
|
16
|
+
const completedActions = data.charts.reduce(
|
|
17
|
+
(sum, chart) => sum + chart.actions.filter((a) => a.metadata.completionStatus).length,
|
|
18
|
+
0,
|
|
19
|
+
)
|
|
20
|
+
const totalBeats = data.charts.reduce((sum, chart) => sum + chart.narrativeBeats.length, 0)
|
|
21
|
+
|
|
22
|
+
const stats = [
|
|
23
|
+
{ label: "Charts", value: totalCharts, icon: Target, color: "text-chart-1" },
|
|
24
|
+
{ label: "Actions", value: `${completedActions}/${totalActions}`, icon: BarChart3, color: "text-chart-2" },
|
|
25
|
+
{ label: "Entities", value: totalEntities, icon: FileText, color: "text-chart-3" },
|
|
26
|
+
{ label: "Relations", value: totalRelations, icon: Network, color: "text-chart-4" },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
31
|
+
{stats.map((stat) => {
|
|
32
|
+
const Icon = stat.icon
|
|
33
|
+
return (
|
|
34
|
+
<Card key={stat.label}>
|
|
35
|
+
<CardHeader className="pb-2">
|
|
36
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">{stat.label}</CardTitle>
|
|
37
|
+
</CardHeader>
|
|
38
|
+
<CardContent>
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
40
|
+
<Icon className={`w-4 h-4 ${stat.color}`} />
|
|
41
|
+
<span className="text-2xl font-bold">{stat.value}</span>
|
|
42
|
+
</div>
|
|
43
|
+
</CardContent>
|
|
44
|
+
</Card>
|
|
45
|
+
)
|
|
46
|
+
})}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type React from "react"
|
|
4
|
+
|
|
5
|
+
import { useCallback } from "react"
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
7
|
+
import { Upload } from "lucide-react"
|
|
8
|
+
|
|
9
|
+
interface FileUploadProps {
|
|
10
|
+
onFileLoad: (content: string, fileName: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FileUpload({ onFileLoad }: FileUploadProps) {
|
|
14
|
+
const handleFileChange = useCallback(
|
|
15
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
16
|
+
const file = e.target.files?.[0]
|
|
17
|
+
if (!file) return
|
|
18
|
+
|
|
19
|
+
const reader = new FileReader()
|
|
20
|
+
reader.onload = (event) => {
|
|
21
|
+
const content = event.target?.result as string
|
|
22
|
+
onFileLoad(content, file.name)
|
|
23
|
+
}
|
|
24
|
+
reader.readAsText(file)
|
|
25
|
+
},
|
|
26
|
+
[onFileLoad],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const handleDrop = useCallback(
|
|
30
|
+
(e: React.DragEvent<HTMLDivElement>) => {
|
|
31
|
+
e.preventDefault()
|
|
32
|
+
const file = e.dataTransfer.files[0]
|
|
33
|
+
if (!file) return
|
|
34
|
+
|
|
35
|
+
const reader = new FileReader()
|
|
36
|
+
reader.onload = (event) => {
|
|
37
|
+
const content = event.target?.result as string
|
|
38
|
+
onFileLoad(content, file.name)
|
|
39
|
+
}
|
|
40
|
+
reader.readAsText(file)
|
|
41
|
+
},
|
|
42
|
+
[onFileLoad],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
}, [])
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Card>
|
|
51
|
+
<CardHeader>
|
|
52
|
+
<CardTitle>Load JSONL File</CardTitle>
|
|
53
|
+
<CardDescription>
|
|
54
|
+
Upload a coaia-narrative memory file (.jsonl) to visualize structural tension charts
|
|
55
|
+
</CardDescription>
|
|
56
|
+
</CardHeader>
|
|
57
|
+
<CardContent>
|
|
58
|
+
<div
|
|
59
|
+
onDrop={handleDrop}
|
|
60
|
+
onDragOver={handleDragOver}
|
|
61
|
+
className="border-2 border-dashed border-border rounded-lg p-12 text-center hover:border-primary/50 transition-colors cursor-pointer"
|
|
62
|
+
>
|
|
63
|
+
<input type="file" accept=".jsonl,.txt" onChange={handleFileChange} className="hidden" id="file-upload" />
|
|
64
|
+
<label htmlFor="file-upload" className="cursor-pointer">
|
|
65
|
+
<Upload className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
|
|
66
|
+
<p className="text-sm font-medium mb-2">Click to upload or drag and drop</p>
|
|
67
|
+
<p className="text-xs text-muted-foreground">JSONL files from coaia-narrative</p>
|
|
68
|
+
</label>
|
|
69
|
+
</div>
|
|
70
|
+
</CardContent>
|
|
71
|
+
</Card>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
4
|
+
import { Badge } from "@/components/ui/badge"
|
|
5
|
+
import type { EntityRecord } from "@/lib/types"
|
|
6
|
+
import { Clock, Compass, Drama, Sparkles } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
interface NarrativeBeatsProps {
|
|
9
|
+
beats: EntityRecord[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function NarrativeBeats({ beats }: NarrativeBeatsProps) {
|
|
13
|
+
if (beats.length === 0) {
|
|
14
|
+
return (
|
|
15
|
+
<Card>
|
|
16
|
+
<CardContent className="py-8">
|
|
17
|
+
<p className="text-sm text-muted-foreground text-center">No narrative beats documented yet</p>
|
|
18
|
+
</CardContent>
|
|
19
|
+
</Card>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="space-y-4">
|
|
25
|
+
{beats.map((beat, idx) => {
|
|
26
|
+
const narrative = beat.metadata.narrative
|
|
27
|
+
const act = beat.metadata.act
|
|
28
|
+
const typeDramatic = beat.metadata.type_dramatic
|
|
29
|
+
const universes = beat.metadata.universes || []
|
|
30
|
+
const timestamp = beat.metadata.timestamp ? new Date(beat.metadata.timestamp).toLocaleString() : null
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Card key={beat.name} className="border-l-4 border-l-chart-3">
|
|
34
|
+
<CardHeader className="bg-chart-3/5">
|
|
35
|
+
<div className="flex items-start justify-between gap-4">
|
|
36
|
+
<div className="flex-1">
|
|
37
|
+
<div className="flex items-center gap-2 mb-2">
|
|
38
|
+
<Drama className="w-5 h-5 text-chart-3" />
|
|
39
|
+
<CardTitle className="text-lg">
|
|
40
|
+
Act {act} - {typeDramatic}
|
|
41
|
+
</CardTitle>
|
|
42
|
+
<Badge variant="outline" className="border-chart-3 text-chart-3">
|
|
43
|
+
Beat {idx + 1}
|
|
44
|
+
</Badge>
|
|
45
|
+
</div>
|
|
46
|
+
{timestamp && (
|
|
47
|
+
<CardDescription className="flex items-center gap-1">
|
|
48
|
+
<Clock className="w-3 h-3" />
|
|
49
|
+
{timestamp}
|
|
50
|
+
</CardDescription>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</CardHeader>
|
|
55
|
+
<CardContent className="space-y-4 pt-4">
|
|
56
|
+
{/* Universes */}
|
|
57
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
58
|
+
<Compass className="w-4 h-4 text-muted-foreground" />
|
|
59
|
+
<span className="text-xs text-muted-foreground">Universes:</span>
|
|
60
|
+
{universes.map((universe: string) => (
|
|
61
|
+
<Badge key={universe} variant="secondary" className="text-xs">
|
|
62
|
+
{universe}
|
|
63
|
+
</Badge>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{/* Description */}
|
|
68
|
+
{narrative?.description && (
|
|
69
|
+
<div>
|
|
70
|
+
<h4 className="text-sm font-semibold mb-2 flex items-center gap-2">
|
|
71
|
+
<Sparkles className="w-4 h-4 text-chart-3" />
|
|
72
|
+
Description
|
|
73
|
+
</h4>
|
|
74
|
+
<p className="text-sm leading-relaxed">{narrative.description}</p>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{/* Prose */}
|
|
79
|
+
{narrative?.prose && (
|
|
80
|
+
<div>
|
|
81
|
+
<h4 className="text-sm font-semibold mb-2">Narrative</h4>
|
|
82
|
+
<div className="bg-chart-3/10 border-l-2 border-l-chart-3 rounded-lg p-4">
|
|
83
|
+
<p className="text-sm leading-relaxed italic">{narrative.prose}</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{/* Lessons */}
|
|
89
|
+
{narrative?.lessons && narrative.lessons.length > 0 && (
|
|
90
|
+
<div>
|
|
91
|
+
<h4 className="text-sm font-semibold mb-2">Lessons Learned</h4>
|
|
92
|
+
<ul className="space-y-2">
|
|
93
|
+
{narrative.lessons.map((lesson: string, lessonIdx: number) => (
|
|
94
|
+
<li key={lessonIdx} className="text-sm flex items-start gap-2 p-2 bg-muted/50 rounded">
|
|
95
|
+
<span className="text-chart-3 mt-0.5 font-bold">•</span>
|
|
96
|
+
<span>{lesson}</span>
|
|
97
|
+
</li>
|
|
98
|
+
))}
|
|
99
|
+
</ul>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</CardContent>
|
|
103
|
+
</Card>
|
|
104
|
+
)
|
|
105
|
+
})}
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
4
|
+
import { Badge } from "@/components/ui/badge"
|
|
5
|
+
import type { ParsedData, Chart, RelationRecord } from "@/lib/types"
|
|
6
|
+
import { ArrowRight } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
interface RelationGraphProps {
|
|
9
|
+
chart: Chart
|
|
10
|
+
data: ParsedData
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function RelationGraph({ chart, data }: RelationGraphProps) {
|
|
14
|
+
const relations = chart.relations
|
|
15
|
+
|
|
16
|
+
if (relations.length === 0) {
|
|
17
|
+
return (
|
|
18
|
+
<Card>
|
|
19
|
+
<CardContent className="py-8">
|
|
20
|
+
<p className="text-sm text-muted-foreground text-center">No relations defined</p>
|
|
21
|
+
</CardContent>
|
|
22
|
+
</Card>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const relationsByType = relations.reduce(
|
|
27
|
+
(acc, rel) => {
|
|
28
|
+
if (!acc[rel.relationType]) {
|
|
29
|
+
acc[rel.relationType] = []
|
|
30
|
+
}
|
|
31
|
+
acc[rel.relationType].push(rel)
|
|
32
|
+
return acc
|
|
33
|
+
},
|
|
34
|
+
{} as Record<string, RelationRecord[]>,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Card>
|
|
39
|
+
<CardHeader>
|
|
40
|
+
<CardTitle>Entity Relations</CardTitle>
|
|
41
|
+
<CardDescription>Connections between chart entities</CardDescription>
|
|
42
|
+
</CardHeader>
|
|
43
|
+
<CardContent className="space-y-6">
|
|
44
|
+
{Object.entries(relationsByType).map(([type, rels]) => (
|
|
45
|
+
<div key={type}>
|
|
46
|
+
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
|
47
|
+
<Badge variant="outline">{type.replace(/_/g, " ")}</Badge>
|
|
48
|
+
<span className="text-muted-foreground">({rels.length})</span>
|
|
49
|
+
</h3>
|
|
50
|
+
<div className="space-y-2">
|
|
51
|
+
{rels.map((rel, idx) => {
|
|
52
|
+
const fromEntity = data.entities.get(rel.from)
|
|
53
|
+
const toEntity = data.entities.get(rel.to)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div key={idx} className="flex items-center gap-3 p-3 bg-muted/30 rounded-lg text-sm">
|
|
57
|
+
<div className="flex-1 min-w-0">
|
|
58
|
+
<div className="font-mono text-xs text-muted-foreground mb-1">{rel.from}</div>
|
|
59
|
+
{fromEntity && (
|
|
60
|
+
<div className="text-xs line-clamp-1">
|
|
61
|
+
{fromEntity.observations[0] || fromEntity.entityType}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
<ArrowRight className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
66
|
+
<div className="flex-1 min-w-0">
|
|
67
|
+
<div className="font-mono text-xs text-muted-foreground mb-1">{rel.to}</div>
|
|
68
|
+
{toEntity && (
|
|
69
|
+
<div className="text-xs line-clamp-1">{toEntity.observations[0] || toEntity.entityType}</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</CardContent>
|
|
79
|
+
</Card>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
ThemeProvider as NextThemesProvider,
|
|
6
|
+
type ThemeProviderProps,
|
|
7
|
+
} from 'next-themes'
|
|
8
|
+
|
|
9
|
+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|
10
|
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
|
11
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
|
14
|
+
secondary:
|
|
15
|
+
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
|
16
|
+
destructive:
|
|
17
|
+
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
|
18
|
+
outline:
|
|
19
|
+
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: 'default',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
function Badge({
|
|
29
|
+
className,
|
|
30
|
+
variant,
|
|
31
|
+
asChild = false,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<'span'> &
|
|
34
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
35
|
+
const Comp = asChild ? Slot : 'span'
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Comp
|
|
39
|
+
data-slot="badge"
|
|
40
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
13
|
+
destructive:
|
|
14
|
+
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
|
15
|
+
outline:
|
|
16
|
+
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
|
17
|
+
secondary:
|
|
18
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
19
|
+
ghost:
|
|
20
|
+
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
|
21
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
25
|
+
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
|
26
|
+
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
|
27
|
+
icon: 'size-9',
|
|
28
|
+
'icon-sm': 'size-8',
|
|
29
|
+
'icon-lg': 'size-10',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: 'default',
|
|
34
|
+
size: 'default',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant,
|
|
42
|
+
size,
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<'button'> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : 'button'
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants }
|