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
+95
View File
@@ -0,0 +1,95 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Message, Conversation } from '@aiui/core/types/message'
export const useChatStore = defineStore('chat', () => {
const conversations = ref<Map<string, Conversation>>(new Map())
const activeConversationId = ref<string | null>(null)
const isStreaming = ref(false)
const panelSide = ref<'left' | 'right'>('right')
const activeConversation = computed(() => {
if (!activeConversationId.value) return null
return conversations.value.get(activeConversationId.value) ?? null
})
const messages = computed(() => activeConversation.value?.messages ?? [])
const conversationList = computed(() =>
Array.from(conversations.value.values()).sort((a, b) => b.updatedAt - a.updatedAt)
)
function createConversation(title = 'New Chat'): string {
const id = crypto.randomUUID()
const conversation: Conversation = {
id,
title,
messages: [],
createdAt: Date.now(),
updatedAt: Date.now(),
}
conversations.value.set(id, conversation)
activeConversationId.value = id
return id
}
function addMessage(conversationId: string, message: Omit<Message, 'id' | 'timestamp'>) {
const conv = conversations.value.get(conversationId)
if (!conv) return
const msg: Message = {
...message,
id: crypto.randomUUID(),
timestamp: Date.now(),
}
conv.messages.push(msg)
conv.updatedAt = Date.now()
if (conv.messages.length === 1 && message.role === 'user') {
conv.title = message.content.slice(0, 60) + (message.content.length > 60 ? '...' : '')
}
return msg
}
function appendToLastMessage(conversationId: string, text: string) {
const conv = conversations.value.get(conversationId)
if (!conv || conv.messages.length === 0) return
const last = conv.messages[conv.messages.length - 1]
last.content += text
}
function switchSide() {
panelSide.value = panelSide.value === 'right' ? 'left' : 'right'
}
function setActiveConversation(id: string) {
if (conversations.value.has(id)) {
activeConversationId.value = id
}
}
function deleteConversation(id: string) {
conversations.value.delete(id)
if (activeConversationId.value === id) {
const remaining = conversationList.value
activeConversationId.value = remaining.length > 0 ? remaining[0].id : null
}
}
return {
conversations,
activeConversationId,
activeConversation,
messages,
conversationList,
isStreaming,
panelSide,
createConversation,
addMessage,
appendToLastMessage,
switchSide,
setActiveConversation,
deleteConversation,
}
})