feat: AI chat now more accurate, has access to data from user, and can generate graphs, charts etc

This commit is contained in:
Jakob Wennberg
2026-02-27 08:48:28 +01:00
parent 0c422fcd25
commit 547fd053ec
24 changed files with 1954 additions and 51 deletions
@@ -0,0 +1,45 @@
'use client'
import type { ArtifactSpec } from '@/types/chat'
import { ChatChart } from './ChatChart'
import { ChatDataTable } from './ChatDataTable'
import { ChatKpiCards } from './ChatKpiCards'
import { ChatAgingBuckets } from './ChatAgingBuckets'
interface ArtifactRendererProps {
artifact: ArtifactSpec
}
export function ArtifactRenderer({ artifact }: ArtifactRendererProps) {
switch (artifact.type) {
case 'bar_chart':
case 'line_chart':
case 'pie_chart':
case 'stacked_bar':
return (
<div className="mt-3 p-3 rounded-lg border border-border bg-card">
<ChatChart artifact={artifact} />
</div>
)
case 'table':
return (
<div className="mt-3">
<ChatDataTable artifact={artifact} />
</div>
)
case 'kpi_cards':
return (
<div className="mt-3">
<ChatKpiCards artifact={artifact} />
</div>
)
case 'aging_buckets':
return (
<div className="mt-3 p-3 rounded-lg border border-border bg-card">
<ChatAgingBuckets artifact={artifact} />
</div>
)
default:
return null
}
}
@@ -0,0 +1,76 @@
'use client'
import type { AgingBucketsArtifact } from '@/types/chat'
const BUCKET_COLORS = [
'bg-green-500',
'bg-yellow-400',
'bg-orange-400',
'bg-red-400',
'bg-red-600',
]
interface ChatAgingBucketsProps {
artifact: AgingBucketsArtifact
}
function formatAmount(amount: number): string {
return new Intl.NumberFormat('sv-SE').format(Math.round(amount))
}
export function ChatAgingBuckets({ artifact }: ChatAgingBucketsProps) {
const { title, buckets, total } = artifact
const maxAmount = Math.max(...buckets.map((b) => b.amount), 1)
return (
<div className="w-full">
<div className="flex items-baseline justify-between mb-3">
<h4 className="text-sm font-semibold">{title}</h4>
<span className="text-sm font-bold tabular-nums">
{formatAmount(total)} kr
</span>
</div>
{/* Stacked bar */}
{total > 0 && (
<div className="flex h-6 rounded-md overflow-hidden mb-3">
{buckets.map((bucket, i) => {
const widthPercent = (bucket.amount / total) * 100
if (widthPercent < 0.5) return null
return (
<div
key={i}
className={`${BUCKET_COLORS[i % BUCKET_COLORS.length]} transition-all`}
style={{ width: `${widthPercent}%` }}
title={`${bucket.label}: ${formatAmount(bucket.amount)} kr`}
/>
)
})}
</div>
)}
{/* Legend */}
<div className="space-y-1.5">
{buckets.map((bucket, i) => (
<div key={i} className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2">
<div
className={`w-3 h-3 rounded-sm ${BUCKET_COLORS[i % BUCKET_COLORS.length]}`}
/>
<span className="text-muted-foreground">{bucket.label}</span>
</div>
<div className="flex items-center gap-3">
<span className="text-muted-foreground">
{bucket.count} st
</span>
<span className="font-medium tabular-nums">
{formatAmount(bucket.amount)} kr
</span>
</div>
</div>
))}
</div>
</div>
)
}
+130
View File
@@ -0,0 +1,130 @@
'use client'
import {
BarChart,
Bar,
LineChart,
Line,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts'
import type { ChartArtifact } from '@/types/chat'
const DEFAULT_COLORS = [
'var(--color-chart-1, #3b82f6)',
'var(--color-chart-2, #10b981)',
'var(--color-chart-3, #f59e0b)',
'var(--color-chart-4, #ef4444)',
'var(--color-chart-5, #8b5cf6)',
'var(--color-chart-6, #ec4899)',
'var(--color-chart-7, #06b6d4)',
'var(--color-chart-8, #84cc16)',
]
function formatValue(value: number, unit?: string): string {
const formatted = new Intl.NumberFormat('sv-SE').format(Math.round(value))
return unit ? `${formatted} ${unit}` : formatted
}
interface ChatChartProps {
artifact: ChartArtifact
}
export function ChatChart({ artifact }: ChatChartProps) {
const { type, title, data, unit, subtitle } = artifact
const chartData = data.map((d, i) => ({
...d,
fill: d.color || DEFAULT_COLORS[i % DEFAULT_COLORS.length],
}))
return (
<div className="w-full">
<div className="mb-3">
<h4 className="text-sm font-semibold">{title}</h4>
{subtitle && <p className="text-xs text-muted-foreground">{subtitle}</p>}
</div>
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
{type === 'pie_chart' ? (
<PieChart>
<Pie
data={chartData}
dataKey="value"
nameKey="label"
cx="50%"
cy="50%"
outerRadius="80%"
label={(props) => {
const name = props.name ?? ''
const percent = typeof props.percent === 'number' ? props.percent : 0
return `${name} (${(percent * 100).toFixed(0)}%)`
}}
labelLine={false}
>
{chartData.map((entry, i) => (
<Cell key={i} fill={entry.fill} />
))}
</Pie>
<Tooltip
formatter={(value) => formatValue(Number(value ?? 0), unit)}
/>
</PieChart>
) : type === 'line_chart' ? (
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis
dataKey="label"
tick={{ fontSize: 11 }}
className="text-muted-foreground"
/>
<YAxis
tick={{ fontSize: 11 }}
tickFormatter={(v) => formatValue(v, unit)}
className="text-muted-foreground"
/>
<Tooltip
formatter={(value) => formatValue(Number(value ?? 0), unit)}
/>
<Line
type="monotone"
dataKey="value"
stroke={DEFAULT_COLORS[0]}
strokeWidth={2}
dot={{ r: 3 }}
/>
</LineChart>
) : (
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis
dataKey="label"
tick={{ fontSize: 11 }}
className="text-muted-foreground"
/>
<YAxis
tick={{ fontSize: 11 }}
tickFormatter={(v) => formatValue(v, unit)}
className="text-muted-foreground"
/>
<Tooltip
formatter={(value) => formatValue(Number(value ?? 0), unit)}
/>
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{chartData.map((entry, i) => (
<Cell key={i} fill={entry.fill} />
))}
</Bar>
</BarChart>
)}
</ResponsiveContainer>
</div>
</div>
)
}
@@ -0,0 +1,80 @@
'use client'
import type { TableArtifact } from '@/types/chat'
interface ChatDataTableProps {
artifact: TableArtifact
}
function formatCell(value: string | number, align?: 'left' | 'right'): string {
if (typeof value === 'number') {
return new Intl.NumberFormat('sv-SE', {
minimumFractionDigits: value % 1 !== 0 ? 2 : 0,
maximumFractionDigits: 2,
}).format(value)
}
return String(value)
}
export function ChatDataTable({ artifact }: ChatDataTableProps) {
const { title, columns, rows, summary_row } = artifact
return (
<div className="w-full">
<h4 className="text-sm font-semibold mb-2">{title}</h4>
<div className="overflow-x-auto rounded-lg border border-border">
<table className="w-full text-xs">
<thead>
<tr className="bg-muted/50">
{columns.map((col) => (
<th
key={col.key}
className={`px-3 py-2 font-medium text-muted-foreground border-b border-border ${
col.align === 'right' ? 'text-right' : 'text-left'
}`}
>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr
key={i}
className="border-b border-border/50 hover:bg-muted/20 transition-colors"
>
{columns.map((col) => (
<td
key={col.key}
className={`px-3 py-1.5 ${
col.align === 'right' ? 'text-right tabular-nums' : 'text-left'
}`}
>
{formatCell(row[col.key], col.align)}
</td>
))}
</tr>
))}
{summary_row && (
<tr className="bg-muted/30 font-semibold">
{columns.map((col) => (
<td
key={col.key}
className={`px-3 py-2 border-t border-border ${
col.align === 'right' ? 'text-right tabular-nums' : 'text-left'
}`}
>
{summary_row[col.key] !== undefined
? formatCell(summary_row[col.key], col.align)
: ''}
</td>
))}
</tr>
)}
</tbody>
</table>
</div>
</div>
)
}
@@ -0,0 +1,47 @@
'use client'
import { TrendingUp, TrendingDown, Minus } from 'lucide-react'
import type { KpiCardsArtifact } from '@/types/chat'
interface ChatKpiCardsProps {
artifact: KpiCardsArtifact
}
export function ChatKpiCards({ artifact }: ChatKpiCardsProps) {
const { title, cards } = artifact
return (
<div className="w-full">
{title && <h4 className="text-sm font-semibold mb-2">{title}</h4>}
<div className="grid grid-cols-2 gap-2">
{cards.map((card, i) => (
<div
key={i}
className="rounded-lg border border-border bg-card p-3"
>
<p className="text-xs text-muted-foreground mb-1">{card.label}</p>
<div className="flex items-baseline gap-2">
<span className="text-lg font-bold tabular-nums">{card.value}</span>
{card.trend && (
<span
className={`flex items-center gap-0.5 text-xs ${
card.trend === 'up'
? 'text-green-600 dark:text-green-400'
: card.trend === 'down'
? 'text-red-600 dark:text-red-400'
: 'text-muted-foreground'
}`}
>
{card.trend === 'up' && <TrendingUp className="h-3 w-3" />}
{card.trend === 'down' && <TrendingDown className="h-3 w-3" />}
{card.trend === 'flat' && <Minus className="h-3 w-3" />}
{card.change}
</span>
)}
</div>
</div>
))}
</div>
</div>
)
}