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'
|
: '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'
|
||||||
|
|
||||||
|
|
|
||||||
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