Files
Trackeep/frontend/src/pages/communication/Chat.tsx
T
Tomas Dvorak 43acbfab15
CI/CD Pipeline / Test (push) Successful in 22m22s
CI/CD Pipeline / Security Scan (push) Successful in 12m37s
CI/CD Pipeline / Build and Push Images (push) Failing after 2m23s
Remove AI/chat/search features, add solidtime integration and workspace setup
2026-06-10 10:41:31 +02:00

1007 lines
44 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createEffect, createResource, createSignal, For, Show, onMount } from 'solid-js'
import { getApiOrigin } from '@/lib/api-url'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Card } from '@/components/ui/Card'
import {
Send,
CheckSquare,
FileText as FileTextIcon,
Sparkles,
ChevronDown,
Trash,
User
} from 'lucide-solid'
interface ChatMessage {
id: number
content: string
role: 'user' | 'assistant'
created_at: string
token_count?: number
context_items?: string[]
}
interface ChatSession {
id: number
title: string
message_count: number
last_message_at?: string
created_at: string
include_bookmarks: boolean
include_tasks: boolean
include_files: boolean
include_notes: boolean
}
const getToken = () => localStorage.getItem('trackeep_token') || localStorage.getItem('token') || ''
const getProviderFromModel = (modelId: string): string => {
const value = modelId.toLowerCase()
if (value.includes('mistral')) return 'mistral'
if (value.includes('grok')) return 'grok'
if (value.includes('deepseek')) return 'deepseek'
if (value.includes('ollama')) return 'ollama'
if (value.includes('openrouter')) return 'openrouter'
return 'longcat'
}
const Chat = () => {
const [activeView, setActiveView] = createSignal<'chat' | 'ai-tools'>('chat')
const [aiTool, setAiTool] = createSignal<'summarizer' | 'tasks' | 'content'>('summarizer')
const [selectedModel, setSelectedModel] = createSignal('longcat')
const [showModelPicker, setShowModelPicker] = createSignal(false)
const [availableProviders, setAvailableProviders] = createSignal<any[]>([])
const [aiSettingsData, setAISettingsData] = createSignal<any>({})
// Load AI providers and settings on mount
onMount(async () => {
parseChatDeeplink()
await loadAIProviders()
await loadAISettings()
})
const loadAIProviders = async () => {
try {
const token = localStorage.getItem('token')
const response = await fetch(`${getApiOrigin()}/api/v1/ai/providers`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
if (response.ok) {
const data = await response.json()
setAvailableProviders(data.providers || [])
}
} catch (error) {
console.error('Failed to load AI providers:', error)
}
}
const loadAISettings = async () => {
try {
const token = localStorage.getItem('token')
const response = await fetch(`${getApiOrigin()}/api/v1/auth/ai/settings`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
if (response.ok) {
const data = await response.json()
setAISettingsData(data)
// Set selected model to first enabled provider
const enabledProviders = Object.entries(data)
.filter(([_, config]: [string, any]) => config.enabled)
.map(([provider]) => provider)
if (enabledProviders.length > 0) {
setSelectedModel(enabledProviders[0])
}
}
} catch (error) {
console.error('Failed to load AI settings:', error)
}
}
// Get available AI models from providers and settings
const getAIModels = () => {
const settings = aiSettingsData()
const providers = availableProviders()
const models: any[] = []
// Add models from enabled providers
if (settings) {
Object.entries(settings).forEach(([provider, config]: [string, any]) => {
if (config.enabled) {
models.push({
id: provider,
name: getProviderDisplayName(provider),
description: getProviderDescription(provider),
provider: getProviderDisplayName(provider),
category: 'available'
})
}
})
}
// Keep disabled providers visible so users can enable them in settings
if (providers.length > 0) {
providers.forEach((provider: any) => {
if (!models.find(m => m.id === provider.id)) {
models.push({
id: provider.id,
name: getProviderDisplayName(provider.id),
description: getProviderDescription(provider.id),
provider: getProviderDisplayName(provider.id),
category: 'disabled'
})
}
})
}
return models
}
const getProviderDisplayName = (provider: string) => {
const names: Record<string, string> = {
mistral: 'Mistral AI',
longcat: 'LongCat AI',
grok: 'Grok AI',
deepseek: 'DeepSeek AI',
ollama: 'Ollama',
openrouter: 'OpenRouter'
}
return names[provider] || provider.charAt(0).toUpperCase() + provider.slice(1)
}
const getProviderDescription = (provider: string) => {
const descriptions: Record<string, string> = {
mistral: 'European AI with multilingual capabilities',
longcat: 'Fast and efficient AI models',
grok: 'xAI\'s conversational AI model',
deepseek: 'Advanced reasoning and coding',
ollama: 'Local AI models',
openrouter: 'Access to multiple AI models'
}
return descriptions[provider] || 'AI provider'
}
const fetchSessions = async () => {
try {
const token = getToken()
const response = await fetch(`${getApiOrigin()}/api/v1/chat/sessions`, {
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
})
if (!response.ok) throw new Error('Failed to fetch sessions')
const data = await response.json()
return Array.isArray(data) ? data : []
} catch (error) {
console.error('Failed to fetch sessions:', error)
return [] as ChatSession[]
}
}
const [sessions, { refetch: refetchSessions }] = createResource(fetchSessions)
const [currentSessionId, setCurrentSessionId] = createSignal<string | null>(null)
const [messages, setMessages] = createSignal<ChatMessage[]>([])
const [deeplinkSessionId, setDeeplinkSessionId] = createSignal<string | null>(null)
const [deeplinkMessageId, setDeeplinkMessageId] = createSignal<number | null>(null)
const [deeplinkResolved, setDeeplinkResolved] = createSignal(false)
const [highlightedMessageId, setHighlightedMessageId] = createSignal<number | null>(null)
const [inputMessage, setInputMessage] = createSignal('')
const [isLoading, setIsLoading] = createSignal(false)
const [showSettings, setShowSettings] = createSignal(false)
const [settingsExpanded, setSettingsExpanded] = createSignal(false)
const [contextSettings, setContextSettings] = createSignal({
bookmarks: true,
tasks: true,
files: true,
notes: true
})
const [aiSettings, setAiSettings] = createSignal({
temperature: 0.7,
maxTokens: 2048,
systemPrompt: 'You are a helpful AI assistant for the Trackeep productivity application. Help users manage their tasks, bookmarks, files, and notes effectively.'
})
const aiTools = [
{ id: 'summarizer', label: 'Content Summarizer', icon: FileTextIcon, description: 'Summarize your notes, documents, and content' },
{ id: 'tasks', label: 'Task Suggestions', icon: CheckSquare, description: 'Get AI-powered task suggestions and organization' },
{ id: 'content', label: 'Content Generation', icon: Sparkles, description: 'Generate content using AI assistance' }
]
const parseChatDeeplink = () => {
const params = new URLSearchParams(window.location.search)
const rawSession = (params.get('session') || '').trim()
const rawMessage = (params.get('message') || '').trim()
setDeeplinkSessionId(rawSession || null)
const parsedMessageId = Number(rawMessage)
setDeeplinkMessageId(Number.isFinite(parsedMessageId) && parsedMessageId > 0 ? parsedMessageId : null)
}
const scrollToHighlightedMessage = (messageId: number) => {
window.requestAnimationFrame(() => {
const element = document.getElementById(`chat-message-${messageId}`)
if (!element) return
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
})
}
const loadSessionMessages = async (sessionId: string) => {
try {
const token = getToken()
const response = await fetch(`${getApiOrigin()}/api/v1/chat/sessions/${sessionId}/messages`, {
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
})
if (!response.ok) {
throw new Error('Failed to fetch messages')
}
const data = await response.json()
const parsedMessages: ChatMessage[] = (Array.isArray(data) ? data : []).map((message: any) => ({
id: Number(message.id || Date.now()),
content: message.content || '',
role: message.role === 'user' ? 'user' : 'assistant',
created_at: message.created_at || new Date().toISOString(),
token_count: message.token_count,
context_items: Array.isArray(message.context_items) ? message.context_items : [],
}))
setMessages(parsedMessages)
const targetMessageId = deeplinkMessageId()
if (targetMessageId && String(deeplinkSessionId() || '') === sessionId) {
if (parsedMessages.some((message) => message.id === targetMessageId)) {
setHighlightedMessageId(targetMessageId)
scrollToHighlightedMessage(targetMessageId)
window.setTimeout(() => {
setHighlightedMessageId((current) => (current === targetMessageId ? null : current))
}, 6000)
} else {
setHighlightedMessageId(null)
}
}
} catch (error) {
console.error('Failed to load session messages:', error)
setMessages([])
}
}
createEffect(() => {
const loadedSessions = sessions()
if (!loadedSessions || loadedSessions.length === 0) {
return
}
if (!deeplinkResolved()) {
const requestedSessionId = deeplinkSessionId()
if (requestedSessionId) {
const matchingSession = loadedSessions.find((session) => String(session.id) === requestedSessionId)
if (matchingSession) {
setCurrentSessionId(requestedSessionId)
setDeeplinkResolved(true)
void loadSessionMessages(requestedSessionId)
return
}
}
setDeeplinkResolved(true)
}
if (currentSessionId()) {
return
}
const firstSessionId = String(loadedSessions[0].id)
setCurrentSessionId(firstSessionId)
void loadSessionMessages(firstSessionId)
})
const handleSendMessage = async () => {
const message = inputMessage().trim()
if (!message || isLoading()) return
// Add user message
const userMessage: ChatMessage = {
id: Date.now(),
content: message,
role: 'user',
created_at: new Date().toISOString()
}
setMessages((prev) => [...prev, userMessage])
setInputMessage('')
setIsLoading(true)
try {
const token = getToken()
const payload: Record<string, any> = {
message,
context: {
bookmarks: contextSettings().bookmarks,
tasks: contextSettings().tasks,
files: contextSettings().files,
notes: contextSettings().notes,
},
provider: getProviderFromModel(selectedModel()),
}
if (currentSessionId()) {
payload.session_id = currentSessionId()
}
const response = await fetch(`${getApiOrigin()}/api/v1/chat/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(payload),
})
if (!response.ok) {
throw new Error('Failed to send message')
}
const data = await response.json()
const sessionId = String(data.session_id || currentSessionId() || '')
if (sessionId) {
setCurrentSessionId(sessionId)
}
const aiResponse: ChatMessage = {
id: Date.now() + 1,
content: data.message || 'No response received.',
role: 'assistant',
created_at: data.timestamp || new Date().toISOString(),
}
setMessages((prev) => [...prev, aiResponse])
await refetchSessions()
} catch (error) {
console.error('Failed to send message:', error)
setMessages((prev) => [
...prev,
{
id: Date.now() + 1,
content: 'Message failed. Please check your AI settings and try again.',
role: 'assistant',
created_at: new Date().toISOString(),
},
])
} finally {
setIsLoading(false)
}
}
return (
<div class="mt-4 pb-32 max-w-7xl mx-auto flex flex-col md:flex-row gap-4">
<div class="w-72 flex-shrink-0 bg-background rounded-lg border shadow-sm overflow-hidden flex flex-col">
<Show when={showSettings()}>
<div class="p-6 border-b bg-muted/30">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-medium text-foreground">
AI Settings
</h3>
<div class="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setSettingsExpanded(!settingsExpanded())}
class="h-6 w-6 p-0"
>
{settingsExpanded() ? '' : '+'}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setShowSettings(false)}
class="h-6 w-6 p-0"
>
×
</Button>
</div>
</div>
{/* Context Settings */}
<div class="mb-6">
<h4 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Context Sources</h4>
<div class="grid grid-cols-2 gap-3">
<label class="flex items-center gap-3 text-sm cursor-pointer bg-background/50 hover:bg-background border border-border/50 rounded-lg p-3 transition-all hover:shadow-sm">
<input
type="checkbox"
checked={contextSettings().bookmarks}
onChange={(e) => setContextSettings(prev => ({ ...prev, bookmarks: (e.target as HTMLInputElement).checked }))}
class="w-4 h-4 text-primary border-border rounded focus:ring-primary focus:ring-2"
/>
<div class="flex items-center gap-2 flex-1">
<span class="font-medium">Bookmarks</span>
</div>
</label>
<label class="flex items-center gap-3 text-sm cursor-pointer bg-background/50 hover:bg-background border border-border/50 rounded-lg p-3 transition-all hover:shadow-sm">
<input
type="checkbox"
checked={contextSettings().tasks}
onChange={(e) => setContextSettings(prev => ({ ...prev, tasks: (e.target as HTMLInputElement).checked }))}
class="w-4 h-4 text-primary border-border rounded focus:ring-primary focus:ring-2"
/>
<div class="flex items-center gap-2 flex-1">
<span class="font-medium">Tasks</span>
</div>
</label>
<label class="flex items-center gap-3 text-sm cursor-pointer bg-background/50 hover:bg-background border border-border/50 rounded-lg p-3 transition-all hover:shadow-sm">
<input
type="checkbox"
checked={contextSettings().files}
onChange={(e) => setContextSettings(prev => ({ ...prev, files: (e.target as HTMLInputElement).checked }))}
class="w-4 h-4 text-primary border-border rounded focus:ring-primary focus:ring-2"
/>
<div class="flex items-center gap-2 flex-1">
<span class="font-medium">Files</span>
</div>
</label>
<label class="flex items-center gap-3 text-sm cursor-pointer bg-background/50 hover:bg-background border border-border/50 rounded-lg p-3 transition-all hover:shadow-sm">
<input
type="checkbox"
checked={contextSettings().notes}
onChange={(e) => setContextSettings(prev => ({ ...prev, notes: (e.target as HTMLInputElement).checked }))}
class="w-4 h-4 text-primary border-border rounded focus:ring-primary focus:ring-2"
/>
<div class="flex items-center gap-2 flex-1">
<span class="font-medium">Notes</span>
</div>
</label>
</div>
<div class="mt-3 p-3 bg-primary/5 border border-primary/20 rounded-lg">
<p class="text-xs text-muted-foreground">
<strong>Tip:</strong> Enable context sources to give the AI access to your data for more personalized responses.
</p>
</div>
</div>
{/* Expanded AI Settings */}
<Show when={settingsExpanded()}>
<div class="space-y-6">
{/* Model Selection */}
<div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Available Models</h4>
<div class="space-y-2 max-h-48 overflow-y-auto">
<For each={getAIModels()}>
{(model) => (
<div
class={`p-3 rounded-lg border cursor-pointer transition-all ${
selectedModel() === model.id
? 'bg-primary/10 border-primary/30'
: 'bg-background/50 hover:bg-background border-border/50'
}`}
onClick={() => setSelectedModel(model.id)}
>
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="font-medium text-sm">{model.name}</div>
<div class="text-xs text-muted-foreground mt-1">{model.description}</div>
<div class="flex items-center gap-2 mt-2">
<span class="text-xs px-2 py-1 bg-primary/10 text-primary rounded-full">
{model.provider}
</span>
<span class="text-xs px-2 py-1 bg-muted text-muted-foreground rounded-full">
{model.category}
</span>
</div>
</div>
{selectedModel() === model.id && (
<div class="ml-2">
<div class="w-2 h-2 bg-primary rounded-full"></div>
</div>
)}
</div>
</div>
)}
</For>
</div>
</div>
{/* AI Parameters */}
<div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">AI Parameters</h4>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">Temperature: {aiSettings().temperature}</label>
<input
type="range"
min="0"
max="2"
step="0.1"
value={aiSettings().temperature}
onInput={(e) => setAiSettings(prev => ({ ...prev, temperature: parseFloat((e.target as HTMLInputElement).value) }))}
class="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer"
/>
<div class="flex justify-between text-xs text-muted-foreground mt-1">
<span>Precise</span>
<span>Creative</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-2">Max Tokens: {aiSettings().maxTokens}</label>
<input
type="range"
min="256"
max="4096"
step="256"
value={aiSettings().maxTokens}
onInput={(e) => setAiSettings(prev => ({ ...prev, maxTokens: parseInt((e.target as HTMLInputElement).value) }))}
class="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer"
/>
<div class="flex justify-between text-xs text-muted-foreground mt-1">
<span>256</span>
<span>4096</span>
</div>
</div>
</div>
</div>
{/* System Prompt */}
<div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">System Prompt</h4>
<textarea
value={aiSettings().systemPrompt}
onInput={(e) => setAiSettings(prev => ({ ...prev, systemPrompt: (e.target as HTMLTextAreaElement).value }))}
class="w-full h-20 p-3 text-sm bg-background/50 border border-border/50 rounded-lg resize-none focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="Enter system prompt..."
/>
</div>
</div>
</Show>
</div>
</Show>
<div class="p-6">
<Button
onClick={() => {
setCurrentSessionId(null)
setMessages([])
}}
class="w-full mb-6 h-11"
>
New Chat
</Button>
<div class="space-y-3 max-h-[calc(100vh-280px)] overflow-y-auto pr-2">
<For each={sessions()}>
{session => (
<div
class={`p-4 rounded-xl cursor-pointer transition-all duration-200 border ${
currentSessionId() === session.id.toString()
? 'bg-primary/10 border-primary/20 shadow-sm'
: 'hover:bg-muted border-transparent hover:shadow-sm'
}`}
onClick={() => {
const sessionId = session.id.toString()
setCurrentSessionId(sessionId)
void loadSessionMessages(sessionId)
}}
>
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0 pr-2">
<h4 class="font-medium truncate mb-1">{session.title}</h4>
<p class="text-sm text-muted-foreground">
{session.message_count} message{session.message_count !== 1 ? 's' : ''}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={async (e: MouseEvent) => {
e.stopPropagation()
try {
const token = getToken()
const response = await fetch(`${getApiOrigin()}/api/v1/chat/sessions/${session.id}`, {
method: 'DELETE',
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
})
if (!response.ok) {
throw new Error('Failed to delete session')
}
if (currentSessionId() === String(session.id)) {
setCurrentSessionId(null)
setMessages([])
}
await refetchSessions()
} catch (error) {
console.error('Delete session failed:', error)
}
}}
class="opacity-0 group-hover:opacity-100 transition-opacity"
>
<Trash class="h-4 w-4" />
</Button>
</div>
</div>
)}
</For>
</div>
</div>
</div>
{/* Chat Area */}
<div class="flex-1 flex flex-col min-w-0 bg-background rounded-lg border shadow-sm overflow-hidden">
<div class="hidden md:flex items-center justify-between p-6 border-b bg-card/95 backdrop-blur-sm">
<div class="flex items-center gap-6">
<div class="flex items-center gap-3">
<div>
<h2 class="font-semibold text-lg">AI Assistant</h2>
<p class="text-sm text-muted-foreground">Your intelligent workspace companion</p>
</div>
</div>
<div class="flex items-center gap-1 p-1 bg-muted rounded-lg">
<button
onClick={() => setActiveView('chat')}
class={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
activeView() === 'chat'
? 'bg-background shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
Chat
</button>
<button
onClick={() => setActiveView('ai-tools')}
class={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
activeView() === 'ai-tools'
? 'bg-background shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
AI Tools
</button>
</div>
</div>
{/* AI Model Picker */}
<div class="relative">
<button
onClick={() => setShowModelPicker(!showModelPicker())}
class="flex items-center gap-2 px-3 py-2 bg-muted hover:bg-muted/80 rounded-lg text-sm transition-colors"
>
<span>{getAIModels().find(m => m.id === selectedModel())?.name || 'Select Model'}</span>
<ChevronDown class="h-4 w-4" />
</button>
<Show when={showModelPicker()}>
<div class="absolute right-0 mt-2 w-72 bg-background border rounded-lg shadow-lg z-50 p-2">
<For each={getAIModels()}>
{model => (
<button
onClick={() => {
setSelectedModel(model.id)
setShowModelPicker(false)
}}
class={`w-full text-left p-3 rounded-lg transition-colors ${
selectedModel() === model.id
? 'bg-primary/10 border border-primary/20'
: 'hover:bg-muted'
}`}
>
<div class="font-medium text-sm">{model.name}</div>
<div class="text-xs text-muted-foreground mt-1">{model.description}</div>
</button>
)}
</For>
</div>
</Show>
</div>
</div>
{/* Main Content Area */}
<div class="flex-1 flex flex-col min-h-0">
<Show when={activeView() === 'chat'}>
<div class="flex-1 overflow-y-auto min-h-0">
<div class="space-y-6 max-w-5xl mx-auto p-6">
<For each={messages()}>
{message => (
<div
class={`flex gap-4 ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
id={`chat-message-${message.id}`}
class={`max-w-[80%] rounded-2xl p-4 shadow-sm transition-all duration-200 hover:shadow-md ${
message.role === 'user'
? 'bg-gradient-to-br from-primary to-primary/90 text-primary-foreground ml-auto'
: 'bg-gradient-to-br from-muted to-muted/90 border border-border/50'
} ${highlightedMessageId() === message.id ? 'ring-2 ring-primary/50 ring-offset-2 ring-offset-background' : ''}`}
>
<div class="flex items-start gap-3">
<div class={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center transition-transform hover:scale-105 ${
message.role === 'user' ? 'bg-primary-foreground/20 ring-2 ring-primary-foreground/30' : 'bg-primary/10 ring-2 ring-primary/20'
}`}>
{message.role === 'user' ? (
<User class="w-4 h-4 text-xs" />
) : (
<Sparkles class="w-4 h-4 text-primary animate-pulse" />
)}
</div>
<div class="flex-1">
<p class="text-sm leading-relaxed whitespace-pre-wrap break-words">{message.content}</p>
<div class="flex items-center gap-2 mt-2">
<p class="text-xs opacity-70">
{new Date(message.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
{message.role === 'user' && (
<div class="w-2 h-2 bg-primary-foreground/50 rounded-full"></div>
)}
</div>
</div>
</div>
</div>
</div>
)}
</For>
</div>
</div>
<div class="border-t bg-card/95 backdrop-blur-sm">
<div class="p-6">
<div class="flex gap-4">
{/* AI Model Switcher */}
<div class="relative">
<button
onClick={() => setShowModelPicker(!showModelPicker())}
class="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-muted to-muted/80 hover:from-muted/90 hover:to-muted/70 rounded-xl text-sm transition-all duration-200 border border-border/50 shadow-sm hover:shadow-md"
>
<Sparkles class="w-4 h-4 transition-transform hover:scale-110" />
<span class="text-sm font-medium">
{getAIModels().find(m => m.id === selectedModel())?.name || 'Select Model'}
</span>
<ChevronDown class={`h-4 w-4 transition-transform duration-200 ${showModelPicker() ? 'rotate-180' : ''}`} />
</button>
<Show when={showModelPicker()}>
<div class="absolute bottom-full left-0 mb-2 w-80 bg-gradient-to-b from-background to-background/95 backdrop-blur-sm border border-border/50 rounded-xl shadow-xl z-50 p-2 max-h-96 overflow-y-auto">
<div class="p-3 border-b border-border/50 mb-2 bg-muted/30 rounded-lg">
<h4 class="text-sm font-semibold text-foreground">Select AI Model</h4>
<p class="text-xs text-muted-foreground">Choose the best model for your needs</p>
</div>
<For each={getAIModels()}>
{model => (
<button
onClick={() => {
setSelectedModel(model.id)
setShowModelPicker(false)
}}
class={`w-full text-left p-3 rounded-lg transition-colors ${
selectedModel() === model.id
? 'bg-primary/10 border border-primary/20'
: 'hover:bg-muted'
}`}
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3 flex-1">
<Sparkles class="w-4 h-4" />
<div class="flex-1">
<div class="font-medium text-sm">{model.name}</div>
<div class="text-xs text-muted-foreground mt-1">{model.description}</div>
<div class="flex items-center gap-2 mt-2">
<span class="text-xs px-2 py-1 bg-gradient-to-r from-primary/10 to-primary/5 text-primary rounded-full border border-primary/20">
{model.provider}
</span>
<span class={`text-xs px-2 py-1 rounded-full border ${
model.category === 'available'
? 'bg-gradient-to-r from-green-50 to-green-100 text-green-700 border-green-200'
: 'bg-gradient-to-r from-muted/50 to-muted text-muted-foreground border-border/50'
}`}>
{model.category === 'available' ? 'Available' : 'Disabled'}
</span>
</div>
</div>
</div>
{selectedModel() === model.id && (
<div class="ml-2">
<div class="w-2 h-2 bg-primary rounded-full animate-pulse"></div>
</div>
)}
</div>
</button>
)}
</For>
</div>
</Show>
</div>
<div class="relative flex-1">
<div class="relative">
<Input
value={inputMessage()}
onInput={(e) => setInputMessage((e.currentTarget as HTMLInputElement).value)}
placeholder="Type your message..."
class="flex-1 pr-12 rounded-xl border-border/50 bg-background/95 backdrop-blur-sm shadow-sm transition-all duration-200 focus:shadow-md focus:border-primary/50"
onKeyDown={(e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey && inputMessage().trim()) {
handleSendMessage()
}
}}
/>
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
<div class="w-1 h-1 bg-muted-foreground/40 rounded-full animate-pulse"></div>
<div class="w-1 h-1 bg-muted-foreground/40 rounded-full animate-pulse" style="animation-delay: 0.2s"></div>
<div class="w-1 h-1 bg-muted-foreground/40 rounded-full animate-pulse" style="animation-delay: 0.4s"></div>
</div>
</div>
</div>
<Button
disabled={isLoading() || !inputMessage().trim()}
onClick={handleSendMessage}
class="rounded-xl px-4 py-2.5 bg-gradient-to-r from-primary to-primary/90 hover:from-primary/90 hover:to-primary shadow-sm hover:shadow-md transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Send class="h-4 w-4" />
<span class="ml-2 text-sm font-medium">Send</span>
</Button>
</div>
</div>
</div>
</Show>
<Show when={activeView() === 'ai-tools'}>
<div class="flex-1 overflow-y-auto p-6 h-[calc(100vh-320px)]">
<div class="max-w-5xl mx-auto">
<div class="mb-8">
<h3 class="text-lg font-semibold mb-2">AI Tools</h3>
<p class="text-muted-foreground">Enhance your productivity with AI-powered tools</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<For each={aiTools}>
{tool => (
<Card
class={`p-6 cursor-pointer transition-all hover:shadow-lg ${
aiTool() === tool.id ? 'ring-2 ring-primary bg-primary/5' : ''
}`}
onClick={() => setAiTool(tool.id as any)}
>
<h4 class="font-semibold mb-2">{tool.label}</h4>
<p class="text-sm text-muted-foreground">{tool.description}</p>
</Card>
)}
</For>
</div>
<Show when={aiTool() === 'summarizer'}>
<Card class="p-6">
<h4 class="text-lg font-semibold mb-4">Content Summarizer</h4>
<p class="text-muted-foreground mb-4">
Get AI-powered summaries of your notes, documents, and bookmarks.
</p>
<div class="space-y-4">
<div class="space-y-2">
<label class="block text-sm font-medium">Content to summarize:</label>
<textarea
placeholder="Paste your content or describe what you want summarized..."
value={inputMessage()}
onInput={(e) => setInputMessage((e.currentTarget as HTMLTextAreaElement).value)}
class="w-full min-h-[100px] p-3 border border-input rounded-md bg-background text-foreground resize-none focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
<div class="flex gap-2">
<Button
onClick={() => setActiveView('chat')}
disabled={!inputMessage().trim()}
>
Summarize In Chat
</Button>
<Button variant="outline" onClick={() => setInputMessage('')}>
Clear
</Button>
</div>
</div>
</Card>
</Show>
<Show when={aiTool() === 'tasks'}>
<Card class="p-6">
<h4 class="text-lg font-semibold mb-4">Task Suggestions</h4>
<p class="text-muted-foreground mb-4">
AI task suggestions are generated from your real workspace context.
</p>
<div class="space-y-4">
<div class="p-4 bg-muted/30 rounded-lg">
<h5 class="font-medium mb-2">No suggestions yet</h5>
<p class="text-sm text-muted-foreground">
Start a chat and ask for task suggestions to generate items from your current data.
</p>
</div>
<div class="flex gap-2">
<Button onClick={() => setActiveView('chat')}>
Open Chat
</Button>
</div>
</div>
</Card>
</Show>
<Show when={aiTool() === 'content'}>
<Card class="p-6">
<h4 class="text-lg font-semibold mb-4">Content Generation</h4>
<p class="text-muted-foreground mb-4">
Generate content using AI assistance for your projects and documentation.
</p>
<div class="space-y-4">
<div class="space-y-2">
<label class="block text-sm font-medium">What to generate:</label>
<textarea
placeholder="Describe the content you want to generate..."
value={inputMessage()}
onInput={(e) => setInputMessage((e.currentTarget as HTMLTextAreaElement).value)}
class="w-full min-h-[100px] p-3 border border-input rounded-md bg-background text-foreground resize-none focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
<div class="space-y-2">
<label class="block text-sm font-medium">Content Type:</label>
<div class="grid grid-cols-2 gap-2">
<Button
variant={inputMessage().toLowerCase().includes('blog') ? 'default' : 'outline'}
onClick={() => setInputMessage('Generate a blog post about: ')}
>
Blog Post
</Button>
<Button
variant={inputMessage().toLowerCase().includes('email') ? 'default' : 'outline'}
onClick={() => setInputMessage('Generate an email about: ')}
>
Email
</Button>
<Button
variant={inputMessage().toLowerCase().includes('documentation') ? 'default' : 'outline'}
onClick={() => setInputMessage('Generate documentation for: ')}
>
Documentation
</Button>
<Button
variant={inputMessage().toLowerCase().includes('social') ? 'default' : 'outline'}
onClick={() => setInputMessage('Generate social media content about: ')}
>
Social Media
</Button>
</div>
</div>
<div class="flex gap-2">
<Button
onClick={() => setActiveView('chat')}
disabled={!inputMessage().trim()}
>
Generate In Chat
</Button>
<Button variant="outline" onClick={() => setInputMessage('')}>
Clear
</Button>
</div>
</div>
</Card>
</Show>
</div>
</div>
</Show>
</div>
</div>
</div>
)
}
export default Chat