Add GlobalChat component with drawer functionality
- Create GlobalChat.svelte with placeholder chat functionality - Create GlobalChatDrawer.svelte as drawer wrapper - Add global chat button to sidebar menu (both mobile and desktop) - Integrate global chat state management in main layout - Include message history, loading states, and error handling - Implement responsive design and proper drawer behavior 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
This commit is contained in:
195
frontend/src/lib/components/chat/GlobalChat.svelte
Normal file
195
frontend/src/lib/components/chat/GlobalChat.svelte
Normal file
@@ -0,0 +1,195 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Send, X, Loader2, MessageCircle } from 'lucide-svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
export let open = false
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: null
|
||||
}>()
|
||||
|
||||
let message = ''
|
||||
let messages: Array<{ role: 'user' | 'assistant'; content: string; timestamp: Date }> = []
|
||||
let loading = writable(false)
|
||||
let chatContainer: HTMLDivElement
|
||||
|
||||
// Placeholder chat history for demo
|
||||
let chatHistory = [
|
||||
{
|
||||
role: 'assistant' as const,
|
||||
content: 'Hello! I\'m your global assistant. How can I help you today?',
|
||||
timestamp: new Date(Date.now() - 60000)
|
||||
}
|
||||
]
|
||||
|
||||
$: messages = [...chatHistory, ...messages]
|
||||
|
||||
function scrollToBottom() {
|
||||
if (chatContainer) {
|
||||
setTimeout(() => {
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
if (!message.trim() || $loading) return
|
||||
|
||||
const userMessage = message.trim()
|
||||
message = ''
|
||||
|
||||
// Add user message
|
||||
messages = [...messages, {
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
timestamp: new Date()
|
||||
}]
|
||||
|
||||
scrollToBottom()
|
||||
|
||||
// Simulate API call with placeholder response
|
||||
loading.set(true)
|
||||
|
||||
try {
|
||||
// Simulate processing delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
// Add placeholder assistant response
|
||||
const responses = [
|
||||
"I understand you're asking about: " + userMessage + ". This is a placeholder response for the global chat feature.",
|
||||
"That's an interesting question! The global chat functionality is currently being developed with placeholder responses.",
|
||||
"Thanks for your message: \"" + userMessage + "\". In the full implementation, this would connect to the AI system.",
|
||||
"I see you mentioned: " + userMessage + ". This global chat drawer is now functional with placeholder logic as requested."
|
||||
]
|
||||
|
||||
const response = responses[Math.floor(Math.random() * responses.length)]
|
||||
|
||||
messages = [...messages, {
|
||||
role: 'assistant',
|
||||
content: response,
|
||||
timestamp: new Date()
|
||||
}]
|
||||
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
sendUserToast('Error sending message', true)
|
||||
} finally {
|
||||
loading.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyPress(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(date: Date) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
messages = []
|
||||
chatHistory = [
|
||||
{
|
||||
role: 'assistant' as const,
|
||||
content: 'Chat cleared! How can I help you now?',
|
||||
timestamp: new Date()
|
||||
}
|
||||
]
|
||||
scrollToBottom()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full bg-surface">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<div class="flex items-center gap-2">
|
||||
<MessageCircle size={18} class="text-primary" />
|
||||
<h2 class="text-lg font-semibold">Global Chat</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color="light"
|
||||
on:click={clearChat}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color="light"
|
||||
iconOnly
|
||||
startIcon={{ icon: X }}
|
||||
on:click={() => dispatch('close')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<div
|
||||
bind:this={chatContainer}
|
||||
class="flex-1 overflow-y-auto p-4 space-y-4"
|
||||
>
|
||||
{#each messages as msg (msg.timestamp.getTime())}
|
||||
<div class={twMerge(
|
||||
"flex flex-col",
|
||||
msg.role === 'user' ? "items-end" : "items-start"
|
||||
)}>
|
||||
<div class={twMerge(
|
||||
"max-w-[80%] p-3 rounded-lg text-sm",
|
||||
msg.role === 'user'
|
||||
? "bg-blue-500 text-white rounded-br-sm"
|
||||
: "bg-gray-100 dark:bg-gray-700 text-primary rounded-bl-sm"
|
||||
)}>
|
||||
<p class="whitespace-pre-wrap">{msg.content}</p>
|
||||
</div>
|
||||
<span class="text-xs text-secondary mt-1">
|
||||
{formatTime(msg.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if $loading}
|
||||
<div class="flex items-start">
|
||||
<div class="bg-gray-100 dark:bg-gray-700 p-3 rounded-lg rounded-bl-sm">
|
||||
<div class="flex items-center gap-2 text-secondary">
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
<span class="text-sm">Thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="p-4 border-t border-gray-200 dark:border-gray-600">
|
||||
<div class="flex gap-2">
|
||||
<textarea
|
||||
bind:value={message}
|
||||
on:keydown={handleKeyPress}
|
||||
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
|
||||
class="flex-1 resize-none border border-gray-300 dark:border-gray-600 rounded-lg p-3 text-sm bg-surface text-primary focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-[44px] max-h-32"
|
||||
rows="1"
|
||||
disabled={$loading}
|
||||
></textarea>
|
||||
<Button
|
||||
size="md"
|
||||
disabled={!message.trim() || $loading}
|
||||
iconOnly
|
||||
startIcon={{ icon: Send }}
|
||||
on:click={sendMessage}
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-secondary mt-2">
|
||||
Global chat with placeholder functionality - ready for AI integration
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
21
frontend/src/lib/components/chat/GlobalChatDrawer.svelte
Normal file
21
frontend/src/lib/components/chat/GlobalChatDrawer.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import GlobalChat from './GlobalChat.svelte'
|
||||
|
||||
export let open = false
|
||||
|
||||
function closeDrawer() {
|
||||
open = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer
|
||||
bind:open
|
||||
size="500px"
|
||||
placement="right"
|
||||
>
|
||||
<DrawerContent title="Global Chat" on:close={closeDrawer}>
|
||||
<GlobalChat {open} on:close={closeDrawer} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -43,7 +43,7 @@
|
||||
import { SUPERADMIN_SETTINGS_HASH, USER_SETTINGS_HASH } from '$lib/components/sidebar/settings'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { syncTutorialsTodos } from '$lib/tutorialUtils'
|
||||
import { ArrowLeft, Search } from 'lucide-svelte'
|
||||
import { ArrowLeft, Search, MessageCircle } from 'lucide-svelte'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import { workspaceAIClients } from '$lib/components/copilot/lib'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -53,10 +53,12 @@
|
||||
import { setContext } from 'svelte'
|
||||
import { base } from '$app/paths'
|
||||
import { Menubar } from '$lib/components/meltComponents'
|
||||
import GlobalChatDrawer from '$lib/components/chat/GlobalChatDrawer.svelte'
|
||||
|
||||
OpenAPI.WITH_CREDENTIALS = true
|
||||
let menuOpen = false
|
||||
let globalSearchModal: GlobalSearchModal | undefined = undefined
|
||||
let globalChatOpen = false
|
||||
let isCollapsed = false
|
||||
let userSettings: UserSettings
|
||||
let superadminSettings: SuperadminSettings
|
||||
@@ -305,6 +307,10 @@
|
||||
globalSearchModal?.openSearchWithPrefilledText(text)
|
||||
}
|
||||
|
||||
function openGlobalChat(): void {
|
||||
globalChatOpen = true
|
||||
}
|
||||
|
||||
setContext('openSearchWithPrefilledText', openSearchModal)
|
||||
|
||||
$: {
|
||||
@@ -449,6 +455,14 @@
|
||||
class="!text-xs"
|
||||
shortcut={`${getModifierKey()}k`}
|
||||
/>
|
||||
<MenuButton
|
||||
stopPropagationOnClick={true}
|
||||
on:click={() => openGlobalChat()}
|
||||
isCollapsed={false}
|
||||
icon={MessageCircle}
|
||||
label="Global Chat"
|
||||
class="!text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarContent
|
||||
@@ -508,6 +522,14 @@
|
||||
class="!text-xs"
|
||||
shortcut={`${getModifierKey()}k`}
|
||||
/>
|
||||
<MenuButton
|
||||
stopPropagationOnClick={true}
|
||||
on:click={() => openGlobalChat()}
|
||||
{isCollapsed}
|
||||
icon={MessageCircle}
|
||||
label="Global Chat"
|
||||
class="!text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarContent
|
||||
@@ -607,6 +629,14 @@
|
||||
class="!text-xs"
|
||||
shortcut={`${getModifierKey()}k`}
|
||||
/>
|
||||
<MenuButton
|
||||
stopPropagationOnClick={true}
|
||||
on:click={() => openGlobalChat()}
|
||||
{isCollapsed}
|
||||
icon={MessageCircle}
|
||||
label="Global Chat"
|
||||
class="!text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SidebarContent
|
||||
@@ -671,3 +701,6 @@
|
||||
</div>
|
||||
</CenteredModal>
|
||||
{/if}
|
||||
|
||||
<!-- Global Chat Drawer -->
|
||||
<GlobalChatDrawer bind:open={globalChatOpen} />
|
||||
|
||||
Reference in New Issue
Block a user