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([]) const [aiSettingsData, setAISettingsData] = createSignal({}) // 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 = { 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 = { 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(null) const [messages, setMessages] = createSignal([]) const [deeplinkSessionId, setDeeplinkSessionId] = createSignal(null) const [deeplinkMessageId, setDeeplinkMessageId] = createSignal(null) const [deeplinkResolved, setDeeplinkResolved] = createSignal(false) const [highlightedMessageId, setHighlightedMessageId] = createSignal(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 = { 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 (

AI Settings

{/* Context Settings */}

Context Sources

Tip: Enable context sources to give the AI access to your data for more personalized responses.

{/* Expanded AI Settings */}
{/* Model Selection */}

Available Models

{(model) => (
setSelectedModel(model.id)} >
{model.name}
{model.description}
{model.provider} {model.category}
{selectedModel() === model.id && (
)}
)}
{/* AI Parameters */}

AI Parameters

setAiSettings(prev => ({ ...prev, temperature: parseFloat((e.target as HTMLInputElement).value) }))} class="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer" />
Precise Creative
setAiSettings(prev => ({ ...prev, maxTokens: parseInt((e.target as HTMLInputElement).value) }))} class="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer" />
256 4096
{/* System Prompt */}

System Prompt