feat: AI chat now more accurate, has access to data from user, and can generate graphs, charts etc
This commit is contained in:
@@ -3,12 +3,14 @@
|
||||
import { useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronDown, ChevronUp, FileText, User, Bot } from 'lucide-react'
|
||||
import { ChevronDown, ChevronUp, FileText, User, Bot, Database, Loader2 } from 'lucide-react'
|
||||
import type { ChatMessage as ChatMessageType, SourceReference } from '@/types/chat'
|
||||
import { ArtifactRenderer } from './artifacts/ArtifactRenderer'
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: ChatMessageType
|
||||
isStreaming?: boolean
|
||||
toolsExecuting?: string[]
|
||||
}
|
||||
|
||||
function SourcesList({ sources }: { sources: SourceReference[] }) {
|
||||
@@ -58,7 +60,36 @@ function SourcesList({ sources }: { sources: SourceReference[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function ChatMessage({ message, isStreaming }: ChatMessageProps) {
|
||||
const TOOL_LABELS: Record<string, string> = {
|
||||
get_invoices: 'Hämtar fakturor',
|
||||
get_supplier_invoices: 'Hämtar leverantörsfakturor',
|
||||
get_account_balances: 'Hämtar kontosaldon',
|
||||
get_transactions: 'Hämtar transaktioner',
|
||||
get_journal_entries: 'Hämtar verifikationer',
|
||||
get_income_statement: 'Genererar resultaträkning',
|
||||
get_balance_sheet: 'Genererar balansräkning',
|
||||
get_vat_summary: 'Beräknar momssammanställning',
|
||||
get_company_overview: 'Hämtar företagsöversikt',
|
||||
get_aging_report: 'Genererar åldersanalys',
|
||||
}
|
||||
|
||||
function ToolExecutingIndicator({ tools }: { tools: string[] }) {
|
||||
if (tools.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 mb-2">
|
||||
{tools.map((toolName, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<Database className="h-3 w-3" />
|
||||
<span>{TOOL_LABELS[toolName] || toolName}...</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ChatMessage({ message, isStreaming, toolsExecuting }: ChatMessageProps) {
|
||||
const isUser = message.role === 'user'
|
||||
|
||||
return (
|
||||
@@ -93,6 +124,11 @@ export function ChatMessage({ message, isStreaming }: ChatMessageProps) {
|
||||
: 'bg-muted/60 text-foreground'
|
||||
)}
|
||||
>
|
||||
{/* Tool execution indicator */}
|
||||
{!isUser && isStreaming && toolsExecuting && toolsExecuting.length > 0 && !message.content && (
|
||||
<ToolExecutingIndicator tools={toolsExecuting} />
|
||||
)}
|
||||
|
||||
<div className={cn(
|
||||
"text-sm break-words",
|
||||
!isUser && "prose prose-sm max-w-none prose-headings:text-foreground prose-headings:font-semibold prose-h1:text-base prose-h2:text-sm prose-h3:text-sm prose-p:text-foreground prose-p:my-1.5 prose-headings:my-2 prose-ul:my-1.5 prose-ol:my-1.5 prose-li:my-0.5 prose-li:text-foreground prose-strong:text-foreground prose-table:my-2 prose-table:text-xs prose-th:px-2 prose-th:py-1 prose-td:px-2 prose-td:py-1 prose-th:bg-muted/50 prose-th:border prose-td:border prose-th:border-border/50 prose-td:border-border/50 prose-th:text-foreground prose-td:text-foreground"
|
||||
@@ -107,6 +143,11 @@ export function ChatMessage({ message, isStreaming }: ChatMessageProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Artifact visualization */}
|
||||
{!isUser && message.artifact && (
|
||||
<ArtifactRenderer artifact={message.artifact} />
|
||||
)}
|
||||
|
||||
{!isUser && message.sources && message.sources.length > 0 && (
|
||||
<SourcesList sources={message.sources} />
|
||||
)}
|
||||
|
||||
@@ -21,6 +21,7 @@ export function ChatPanel({ className }: ChatPanelProps) {
|
||||
isLoading,
|
||||
isStreaming,
|
||||
error,
|
||||
toolsExecuting,
|
||||
sendMessage,
|
||||
clearChat,
|
||||
} = useChatStream({
|
||||
@@ -90,10 +91,22 @@ export function ChatPanel({ className }: ChatPanelProps) {
|
||||
</p>
|
||||
<div className="mt-6 space-y-2 w-full max-w-xs">
|
||||
<SuggestionButton
|
||||
onClick={() => sendMessage('Hur fungerar momsen på mina fakturor?')}
|
||||
onClick={() => sendMessage('Hur går det för mitt företag?')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Hur fungerar momsen på mina fakturor?
|
||||
Hur går det för mitt företag?
|
||||
</SuggestionButton>
|
||||
<SuggestionButton
|
||||
onClick={() => sendMessage('Visa mina senaste fakturor')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Visa mina senaste fakturor
|
||||
</SuggestionButton>
|
||||
<SuggestionButton
|
||||
onClick={() => sendMessage('Hur ser min resultaträkning ut?')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Hur ser min resultaträkning ut?
|
||||
</SuggestionButton>
|
||||
<SuggestionButton
|
||||
onClick={() => sendMessage('Vad kan jag dra av som företagare?')}
|
||||
@@ -101,12 +114,6 @@ export function ChatPanel({ className }: ChatPanelProps) {
|
||||
>
|
||||
Vad kan jag dra av som företagare?
|
||||
</SuggestionButton>
|
||||
<SuggestionButton
|
||||
onClick={() => sendMessage('När måste jag momsregistrera mig?')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
När måste jag momsregistrera mig?
|
||||
</SuggestionButton>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -120,6 +127,13 @@ export function ChatPanel({ className }: ChatPanelProps) {
|
||||
index === messages.length - 1 &&
|
||||
message.role === 'assistant'
|
||||
}
|
||||
toolsExecuting={
|
||||
isStreaming &&
|
||||
index === messages.length - 1 &&
|
||||
message.role === 'assistant'
|
||||
? toolsExecuting
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
import type { ChatMessage, SourceReference, ChatSession } from '@/types/chat'
|
||||
import type { ChatMessage, SourceReference, ArtifactSpec } from '@/types/chat'
|
||||
|
||||
interface UseChatStreamOptions {
|
||||
onError?: (error: string) => void
|
||||
@@ -13,6 +13,7 @@ interface UseChatStreamReturn {
|
||||
isStreaming: boolean
|
||||
sessionId: string | null
|
||||
error: string | null
|
||||
toolsExecuting: string[]
|
||||
sendMessage: (message: string) => Promise<void>
|
||||
loadSession: (sessionId: string) => Promise<void>
|
||||
clearChat: () => void
|
||||
@@ -25,6 +26,7 @@ export function useChatStream(options: UseChatStreamOptions = {}): UseChatStream
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
const [sessionId, setSessionId] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [toolsExecuting, setToolsExecuting] = useState<string[]>([])
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
const sendMessage = useCallback(async (message: string) => {
|
||||
@@ -86,6 +88,7 @@ export function useChatStream(options: UseChatStreamOptions = {}): UseChatStream
|
||||
const decoder = new TextDecoder()
|
||||
let accumulatedContent = ''
|
||||
let sources: SourceReference[] = []
|
||||
let artifact: ArtifactSpec | null = null
|
||||
let newSessionId = sessionId
|
||||
let messageId: string | null = null
|
||||
|
||||
@@ -106,6 +109,7 @@ export function useChatStream(options: UseChatStreamOptions = {}): UseChatStream
|
||||
setSessionId(data.session_id)
|
||||
} else if (data.type === 'content') {
|
||||
accumulatedContent += data.content
|
||||
setToolsExecuting([]) // Clear tool indicators when content starts
|
||||
setMessages((prev) => {
|
||||
const newMessages = [...prev]
|
||||
const lastMsg = newMessages[newMessages.length - 1]
|
||||
@@ -124,8 +128,21 @@ export function useChatStream(options: UseChatStreamOptions = {}): UseChatStream
|
||||
}
|
||||
return newMessages
|
||||
})
|
||||
} else if (data.type === 'tool_start') {
|
||||
setToolsExecuting((prev) => [...prev, data.toolName])
|
||||
} else if (data.type === 'artifact') {
|
||||
artifact = data.artifact
|
||||
setMessages((prev) => {
|
||||
const newMessages = [...prev]
|
||||
const lastMsg = newMessages[newMessages.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
lastMsg.artifact = artifact
|
||||
}
|
||||
return newMessages
|
||||
})
|
||||
} else if (data.type === 'done') {
|
||||
messageId = data.message_id
|
||||
setToolsExecuting([])
|
||||
// Update the message IDs with real values
|
||||
setMessages((prev) => {
|
||||
const newMessages = [...prev]
|
||||
@@ -207,6 +224,7 @@ export function useChatStream(options: UseChatStreamOptions = {}): UseChatStream
|
||||
isStreaming,
|
||||
sessionId,
|
||||
error,
|
||||
toolsExecuting,
|
||||
sendMessage,
|
||||
loadSession,
|
||||
clearChat,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
|
||||
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
|
||||
import { MessageSquare } from 'lucide-react'
|
||||
import { ChatPanel } from '@/components/chat/ChatPanel'
|
||||
|
||||
export default function AiChatWorkspace({ userId }: WorkspaceComponentProps) {
|
||||
return (
|
||||
<EmptyExtensionState
|
||||
title="AI-assistent"
|
||||
description="Använd chattwidgeten i nedre högra hörnet för att ställa frågor om bokföring och skatt."
|
||||
icon={<MessageSquare className="h-12 w-12 text-muted-foreground/40 mb-4" />}
|
||||
/>
|
||||
<div className="h-[calc(100vh-10rem)] max-w-4xl mx-auto">
|
||||
<ChatPanel className="h-full rounded-lg border border-border bg-background shadow-sm" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user