feat(app): glassmorphism chat UI with widget embed system

- Glassmorphism chat window matching Proux design rules (glass layers,
  gradient backgrounds, glow effects, blur intensities, inner glows)
- Conversation ID displayed in header, side-switching (left/right layout)
- Chat store with Pinia: conversations, messages, streaming state
- OpenRouter AI integration with SSE streaming (Llama 4 Maverick free model)
- Chat components: ChatWindow, ChatHeader, ChatInput, ChatMessage,
  StreamingDots (typing indicator)
- Embeddable widget system: floating action button (FAB) + modal popup
  with scale-in animation, side-aware positioning
- Widget demo page (/widget-demo) showing AIUI embedded in a mock "Acme App"
  with documentation of 4 integration methods: script tag, web component,
  npm package, and iframe
- Theme composable with dark/light mode, system preference detection
- Custom CSS: glass variants (subtle/medium/strong/light/dark), glow effects,
  gradient text, animation keyframes, thin scrollbar
- Responsive: mobile-first, 44px touch targets, dvh viewport

Made-with: Cursor
This commit is contained in:
Dorian
2026-03-02 14:20:34 +00:00
parent c28e6dd811
commit 27864cf92e
16 changed files with 1057 additions and 32 deletions
@@ -0,0 +1,43 @@
<template>
<div
class="flex animate-fade-up"
:class="isUser ? 'justify-end' : 'justify-start'"
:style="{ animationDelay: `${index * 30}ms` }"
>
<div
class="max-w-[85%] md:max-w-[70%] rounded-2xl px-4 py-3 transition-all duration-200"
:class="bubbleClasses"
>
<p class="text-sm leading-relaxed whitespace-pre-wrap break-words">{{ message.content }}</p>
<div class="flex items-center gap-2 mt-1.5">
<span class="text-[10px] opacity-40 select-none">{{ formattedTime }}</span>
<span v-if="isUser && message.status" class="text-[10px] opacity-30">
{{ message.status === 'sent' ? '' : message.status === 'delivered' ? '' : '' }}
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { Message } from '@aiui/core/types/message'
const props = defineProps<{
message: Message
index: number
}>()
const isUser = computed(() => props.message.role === 'user')
const bubbleClasses = computed(() =>
isUser.value
? 'glass-strong text-white rounded-br-md'
: 'glass-dark text-gray-100 rounded-bl-md'
)
const formattedTime = computed(() => {
const d = new Date(props.message.timestamp)
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
})
</script>