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:
parent
2366a44280
commit
81db5d2d9f
2 changed files with 118 additions and 2 deletions
|
|
@ -158,7 +158,7 @@
|
|||
: 'bg-muted'
|
||||
]"
|
||||
>
|
||||
<p class="text-sm">{{ message.content }}</p>
|
||||
<ChatMessageContent :content="message.content" />
|
||||
<p class="text-xs opacity-70 mt-1">
|
||||
{{ formatTime(message.created_at) }}
|
||||
</p>
|
||||
|
|
@ -325,7 +325,7 @@
|
|||
: 'bg-muted'
|
||||
]"
|
||||
>
|
||||
<p class="text-sm">{{ message.content }}</p>
|
||||
<ChatMessageContent :content="message.content" />
|
||||
<p class="text-xs opacity-70 mt-1">
|
||||
{{ formatTime(message.created_at) }}
|
||||
</p>
|
||||
|
|
@ -376,6 +376,7 @@ import { Badge } from '@/components/ui/badge'
|
|||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { useChat } from '../composables/useChat'
|
||||
import ChatMessageContent from './ChatMessageContent.vue'
|
||||
|
||||
import { useFuzzySearch } from '@/composables/useFuzzySearch'
|
||||
|
||||
|
|
|
|||
115
src/modules/chat/components/ChatMessageContent.vue
Normal file
115
src/modules/chat/components/ChatMessageContent.vue
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue