Add styled order message cards in chat

- Create ChatMessageContent component to detect and render order messages
- Display order details in a clean card format instead of raw JSON
- Show item count, shipping zone with truck icon, and short order ID
- Falls back to plain text for non-order messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
padreug 2026-01-07 01:15:53 +01:00
parent 2366a44280
commit 81db5d2d9f
2 changed files with 118 additions and 2 deletions

View file

@ -158,7 +158,7 @@
: 'bg-muted' : 'bg-muted'
]" ]"
> >
<p class="text-sm">{{ message.content }}</p> <ChatMessageContent :content="message.content" />
<p class="text-xs opacity-70 mt-1"> <p class="text-xs opacity-70 mt-1">
{{ formatTime(message.created_at) }} {{ formatTime(message.created_at) }}
</p> </p>
@ -325,7 +325,7 @@
: 'bg-muted' : 'bg-muted'
]" ]"
> >
<p class="text-sm">{{ message.content }}</p> <ChatMessageContent :content="message.content" />
<p class="text-xs opacity-70 mt-1"> <p class="text-xs opacity-70 mt-1">
{{ formatTime(message.created_at) }} {{ formatTime(message.created_at) }}
</p> </p>
@ -376,6 +376,7 @@ import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { useChat } from '../composables/useChat' import { useChat } from '../composables/useChat'
import ChatMessageContent from './ChatMessageContent.vue'
import { useFuzzySearch } from '@/composables/useFuzzySearch' import { useFuzzySearch } from '@/composables/useFuzzySearch'

View file

@ -0,0 +1,115 @@
<template>
<!-- Order Message -->
<div v-if="parsedOrder" class="min-w-[200px]">
<div class="flex items-center gap-2 font-semibold text-sm mb-3">
<ShoppingBag class="w-4 h-4" />
<span>Order Placed</span>
</div>
<div class="text-xs space-y-2">
<!-- Items -->
<div v-if="parsedOrder.items?.length" class="space-y-1">
<div
v-for="(item, index) in parsedOrder.items"
:key="index"
class="flex items-center gap-2"
>
<span class="opacity-70">{{ item.quantity }}x</span>
<span>Item</span>
</div>
</div>
<!-- Divider -->
<div class="border-t border-current opacity-20 my-2" />
<!-- Shipping -->
<div v-if="shippingLabel" class="flex items-center gap-2">
<Truck class="w-3 h-3 opacity-70" />
<span>{{ shippingLabel }}</span>
</div>
<!-- Order Reference -->
<div class="opacity-60 text-[10px] font-mono mt-2">
#{{ shortOrderId }}
</div>
</div>
</div>
<!-- Regular Text Message -->
<p v-else class="text-sm whitespace-pre-wrap break-words">{{ content }}</p>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ShoppingBag, Truck } from 'lucide-vue-next'
interface OrderItem {
product_id: string
quantity: number
}
interface OrderContact {
name?: string
email?: string
}
interface ParsedOrder {
type: number
id: string
items?: OrderItem[]
contact?: OrderContact
shipping_id?: string
}
const props = defineProps<{
content: string
}>()
// Try to parse the content as an order message
const parsedOrder = computed<ParsedOrder | null>(() => {
try {
// Check if content looks like JSON
const trimmed = props.content.trim()
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
return null
}
const parsed = JSON.parse(trimmed)
// Validate it's an order message (has type and id fields)
if (typeof parsed.type === 'number' && typeof parsed.id === 'string' && parsed.id.startsWith('order_')) {
return parsed as ParsedOrder
}
return null
} catch {
return null
}
})
// Format shipping label
const shippingLabel = computed(() => {
if (!parsedOrder.value?.shipping_id) return null
const id = parsedOrder.value.shipping_id
// Extract zone name if it follows the pattern "zonename-hash"
if (id.includes('-')) {
const zoneName = id.split('-')[0]
// Capitalize first letter
return zoneName.charAt(0).toUpperCase() + zoneName.slice(1)
}
return 'Standard'
})
// Short order ID for display
const shortOrderId = computed(() => {
if (!parsedOrder.value?.id) return ''
// Extract the unique part from "order_timestamp_randomstring"
const parts = parsedOrder.value.id.split('_')
if (parts.length >= 3) {
return parts[2].slice(0, 8) // Just the random part
}
return parsedOrder.value.id.slice(-8)
})
</script>