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:
@@ -1,43 +1,94 @@
|
||||
<template>
|
||||
<div class="flex h-full">
|
||||
<main class="flex-1 flex flex-col min-w-0">
|
||||
<header class="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-800">
|
||||
<h1 class="text-lg font-semibold">AIUI</h1>
|
||||
<span class="text-xs text-gray-400">Phase 0 — Foundation</span>
|
||||
</header>
|
||||
<div class="h-full flex flex-col bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 relative overflow-hidden">
|
||||
<div class="absolute inset-0 pointer-events-none">
|
||||
<div class="absolute top-[-20%] left-[-10%] w-[500px] h-[500px] rounded-full bg-accent/5 blur-[120px]" />
|
||||
<div class="absolute bottom-[-20%] right-[-10%] w-[600px] h-[600px] rounded-full bg-info/5 blur-[120px]" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<div class="max-w-3xl mx-auto space-y-4">
|
||||
<p class="text-sm text-gray-500 text-center py-12">
|
||||
Start a conversation. Ask about films, code, or anything else.
|
||||
<div class="relative z-10 flex-1 flex h-full p-3 md:p-4 gap-3 md:gap-4" :class="layoutClasses">
|
||||
<aside
|
||||
v-if="showSidebar"
|
||||
class="hidden lg:flex w-64 xl:w-72 shrink-0 flex-col glass rounded-2xl overflow-hidden"
|
||||
>
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<h1 class="text-lg font-bold gradient-text">AIUI</h1>
|
||||
<button
|
||||
class="p-2 rounded-lg text-white/40 hover:text-white hover:bg-white/5 transition-all"
|
||||
aria-label="New chat"
|
||||
@click="chatStore.createConversation()"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto scrollbar-thin px-2 pb-2 space-y-1">
|
||||
<button
|
||||
v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
class="w-full text-left px-3 py-2.5 rounded-xl text-sm transition-all duration-200 truncate"
|
||||
:class="conv.id === chatStore.activeConversationId
|
||||
? 'glass-strong text-white'
|
||||
: 'text-white/50 hover:text-white/80 hover:bg-white/5'"
|
||||
@click="chatStore.setActiveConversation(conv.id)"
|
||||
>
|
||||
{{ conv.title }}
|
||||
</button>
|
||||
|
||||
<p
|
||||
v-if="conversations.length === 0"
|
||||
class="text-xs text-white/20 text-center py-8"
|
||||
>
|
||||
No conversations yet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-800 p-4">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div
|
||||
class="flex items-end gap-2 rounded-2xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-colors"
|
||||
>
|
||||
<textarea
|
||||
rows="1"
|
||||
placeholder="Message AIUI..."
|
||||
class="flex-1 resize-none bg-transparent text-base outline-none placeholder:text-gray-400 min-h-[24px] max-h-[200px]"
|
||||
/>
|
||||
<button
|
||||
class="shrink-0 w-8 h-8 flex items-center justify-center rounded-lg bg-primary text-white hover:bg-primary-dark transition-colors"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7-7l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="p-3 border-t border-white/5">
|
||||
<div class="flex items-center gap-2 px-2 py-1.5 rounded-lg text-[11px] text-white/25">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success animate-pulse" />
|
||||
<span>OpenRouter connected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 min-w-0 flex flex-col glass rounded-2xl overflow-hidden">
|
||||
<ChatWindow
|
||||
:side="panelSide"
|
||||
@switch-side="chatStore.switchSide()"
|
||||
/>
|
||||
</main>
|
||||
|
||||
<aside
|
||||
v-if="false"
|
||||
class="hidden xl:flex w-80 shrink-0 flex-col glass rounded-2xl overflow-hidden"
|
||||
>
|
||||
<div class="p-4 text-center text-white/20 text-sm">
|
||||
Content panel (coming soon)
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import ChatWindow from '@/components/chat/ChatWindow.vue'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { initTheme } = useTheme()
|
||||
|
||||
const panelSide = computed(() => chatStore.panelSide)
|
||||
const conversations = computed(() => chatStore.conversationList)
|
||||
const showSidebar = true
|
||||
|
||||
const layoutClasses = computed(() =>
|
||||
panelSide.value === 'left' ? 'flex-row-reverse' : 'flex-row'
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initTheme()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="min-h-full bg-white text-gray-900 font-sans">
|
||||
<nav class="border-b border-gray-200 bg-white sticky top-0 z-30">
|
||||
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center">
|
||||
<span class="text-white text-sm font-bold">A</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold">Acme App</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-6 text-sm text-gray-600">
|
||||
<a href="#" class="hover:text-gray-900">Dashboard</a>
|
||||
<a href="#" class="hover:text-gray-900">Projects</a>
|
||||
<a href="#" class="hover:text-gray-900">Settings</a>
|
||||
<RouterLink to="/" class="text-accent font-medium hover:underline">
|
||||
← Back to AIUI
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-6 py-12">
|
||||
<div class="mb-12">
|
||||
<h1 class="text-3xl font-bold mb-2">Widget Integration Demo</h1>
|
||||
<p class="text-gray-500 max-w-2xl">
|
||||
This page simulates a third-party web application with the AIUI chat widget embedded.
|
||||
Click the floating button in the bottom corner to open the AI assistant.
|
||||
You can also switch which side it appears on.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16">
|
||||
<div v-for="n in 6" :key="n" class="bg-gray-50 rounded-2xl p-6 border border-gray-100">
|
||||
<div class="w-10 h-10 rounded-xl bg-blue-100 text-blue-600 flex items-center justify-center mb-4">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="font-semibold mb-1">Project {{ n }}</h3>
|
||||
<p class="text-sm text-gray-500 mb-3">
|
||||
Sample content card demonstrating how the widget overlays existing UI without disruption.
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2.5 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-700">Active</span>
|
||||
<span class="text-xs text-gray-400">Updated 2h ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-2xl border border-gray-200 p-8 mb-16">
|
||||
<h2 class="text-xl font-bold mb-4">How to embed AIUI in your app</h2>
|
||||
<div class="space-y-6 text-sm text-gray-700">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Option 1: Script Tag (simplest)</h3>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded-lg p-4 overflow-x-auto text-xs"><code><script src="https://cdn.example.com/aiui-widget.js"></script>
|
||||
<script>
|
||||
AIUI.init({
|
||||
apiKey: 'your-openrouter-key',
|
||||
position: 'bottom-right', // or 'bottom-left'
|
||||
theme: 'dark',
|
||||
})
|
||||
</script></code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Option 2: Web Component</h3>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded-lg p-4 overflow-x-auto text-xs"><code><!-- Load the custom element -->
|
||||
<script type="module" src="https://cdn.example.com/aiui-element.js"></script>
|
||||
|
||||
<!-- Use anywhere in your HTML -->
|
||||
<aiui-chat
|
||||
api-key="your-key"
|
||||
position="bottom-right"
|
||||
theme="dark"
|
||||
></aiui-chat></code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Option 3: npm package (Vue/React/Svelte)</h3>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded-lg p-4 overflow-x-auto text-xs"><code>npm install @aiui/widget
|
||||
|
||||
// Vue 3
|
||||
import { AIUIWidget } from '@aiui/widget'
|
||||
app.use(AIUIWidget, {
|
||||
apiKey: 'your-key',
|
||||
position: 'bottom-right',
|
||||
})
|
||||
|
||||
// React
|
||||
import { AIUIProvider } from '@aiui/widget/react'
|
||||
<AIUIProvider apiKey="your-key" position="bottom-right" /></code></pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Option 4: iframe (maximum isolation)</h3>
|
||||
<pre class="bg-gray-900 text-gray-100 rounded-lg p-4 overflow-x-auto text-xs"><code><iframe
|
||||
src="https://app.aiui.dev/embed?key=your-key&theme=dark"
|
||||
style="position:fixed;bottom:80px;right:24px;width:420px;height:640px;
|
||||
border:none;border-radius:16px;z-index:9999;"
|
||||
allow="microphone"
|
||||
></iframe></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-2xl border border-gray-200 p-8">
|
||||
<h2 class="text-xl font-bold mb-4">Technical Architecture</h2>
|
||||
<div class="grid md:grid-cols-2 gap-8 text-sm text-gray-700">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Script Tag / npm</h3>
|
||||
<ul class="space-y-1 list-disc list-inside">
|
||||
<li>Injects a Vue micro-app into a shadow DOM container</li>
|
||||
<li>Styles are encapsulated — no CSS conflicts with host</li>
|
||||
<li>Communicates via CustomEvents or a global AIUI API object</li>
|
||||
<li>Host app can pass context (current page, user info)</li>
|
||||
<li>Smallest footprint: ~80KB gzipped</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Web Component</h3>
|
||||
<ul class="space-y-1 list-disc list-inside">
|
||||
<li>Framework-agnostic Custom Element (works in any HTML)</li>
|
||||
<li>Shadow DOM encapsulation by default</li>
|
||||
<li>Attributes for configuration, events for callbacks</li>
|
||||
<li>Can be lazy-loaded with dynamic import()</li>
|
||||
<li>Works in React, Angular, Svelte, plain HTML</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">iframe</h3>
|
||||
<ul class="space-y-1 list-disc list-inside">
|
||||
<li>Maximum isolation — separate browsing context</li>
|
||||
<li>Zero risk of CSS/JS conflicts</li>
|
||||
<li>Communication via postMessage API</li>
|
||||
<li>API key stays on embedded origin (more secure)</li>
|
||||
<li>Slightly larger overhead, but simplest integration</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Host Communication</h3>
|
||||
<ul class="space-y-1 list-disc list-inside">
|
||||
<li>Host can send page context to AI for better answers</li>
|
||||
<li>Widget can trigger actions in host app via callbacks</li>
|
||||
<li>Conversation history syncs via encrypted IndexedDB</li>
|
||||
<li>Deep-link support: <code class="bg-gray-200 px-1 rounded">?aiui=open&q=help</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AIUIWidget />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
import AIUIWidget from '@/components/widget/AIUIWidget.vue'
|
||||
</script>
|
||||
Reference in New Issue
Block a user