Procházet zdrojové kódy

fix: replace lucide-react imports with inline SVG icons

The project uses inline SVGs instead of lucide-react. Updated all Chat
components and LlmProviders page to use inline SVG icon components.
fszontagh před 6 měsíci
rodič
revize
4a83c6b5f6

+ 9 - 2
webui/src/components/Chat/ChatIcon.tsx

@@ -1,9 +1,16 @@
 // Minimized chat icon button (FAB)
 
-import { MessageSquare } from 'lucide-react'
 import { useChat } from '@/contexts/ChatContext'
 import { usePermissions } from '@/hooks/usePermissions'
 
+function MessageSquareIcon({ className = 'h-6 w-6' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
+    </svg>
+  )
+}
+
 export function ChatIcon() {
   const { toggleChat, isMinimized } = useChat()
   const { hasPermission } = usePermissions()
@@ -23,7 +30,7 @@ export function ChatIcon() {
       className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg transition-all hover:bg-blue-700 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
       aria-label="Open chat assistant"
     >
-      <MessageSquare className="h-6 w-6" />
+      <MessageSquareIcon className="h-6 w-6" />
     </button>
   )
 }

+ 18 - 3
webui/src/components/Chat/ChatInput.tsx

@@ -1,9 +1,24 @@
 // Chat input field with send button
 
 import { useState, useRef, KeyboardEvent } from 'react'
-import { Send, Loader2 } from 'lucide-react'
 import { useChat } from '@/contexts/ChatContext'
 
+function SendIcon({ className = 'h-5 w-5' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
+    </svg>
+  )
+}
+
+function Loader2Icon({ className = 'h-5 w-5' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
+    </svg>
+  )
+}
+
 export function ChatInput() {
   const [input, setInput] = useState('')
   const textareaRef = useRef<HTMLTextAreaElement>(null)
@@ -63,9 +78,9 @@ export function ChatInput() {
           aria-label="Send message"
         >
           {isStreaming ? (
-            <Loader2 className="h-5 w-5 animate-spin" />
+            <Loader2Icon className="h-5 w-5 animate-spin" />
           ) : (
-            <Send className="h-5 w-5" />
+            <SendIcon className="h-5 w-5" />
           )}
         </button>
       </div>

+ 18 - 4
webui/src/components/Chat/ChatMessage.tsx

@@ -1,9 +1,24 @@
 // Individual chat message bubble
 
 import { useMemo } from 'react'
-import { User, Bot } from 'lucide-react'
 import type { ChatMessage as ChatMessageType } from '@/types/chat'
 
+function UserIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
+    </svg>
+  )
+}
+
+function BotIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
+    </svg>
+  )
+}
+
 interface ChatMessageProps {
   message: ChatMessageType
   isStreaming?: boolean
@@ -12,7 +27,6 @@ interface ChatMessageProps {
 
 export function ChatMessage({ message, isStreaming, streamingContent }: ChatMessageProps) {
   const isUser = message.role === 'user'
-  const isAssistant = message.role === 'assistant'
 
   const content = useMemo(() => {
     if (isStreaming && streamingContent) {
@@ -35,9 +49,9 @@ export function ChatMessage({ message, isStreaming, streamingContent }: ChatMess
         }`}
       >
         {isUser ? (
-          <User className="h-4 w-4 text-white" />
+          <UserIcon className="h-4 w-4 text-white" />
         ) : (
-          <Bot className="h-4 w-4 text-white" />
+          <BotIcon className="h-4 w-4 text-white" />
         )}
       </div>
 

+ 9 - 2
webui/src/components/Chat/ChatMessageList.tsx

@@ -3,7 +3,14 @@
 import { useRef, useEffect } from 'react'
 import { useChat } from '@/contexts/ChatContext'
 import { ChatMessage } from './ChatMessage'
-import { Loader2 } from 'lucide-react'
+
+function Loader2Icon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
+    </svg>
+  )
+}
 
 export function ChatMessageList() {
   const { messages, isStreaming, streamingContent } = useChat()
@@ -40,7 +47,7 @@ export function ChatMessageList() {
         {/* Streaming indicator when no assistant message yet */}
         {isStreaming && (messages.length === 0 || messages[messages.length - 1]?.role === 'user') && (
           <div className="flex items-center gap-2 text-gray-500">
-            <Loader2 className="h-4 w-4 animate-spin" />
+            <Loader2Icon className="h-4 w-4 animate-spin" />
             <span className="text-sm">Thinking...</span>
           </div>
         )}

+ 18 - 3
webui/src/components/Chat/ChatWindow.tsx

@@ -1,7 +1,22 @@
 // Full chat window component
 
-import { X, Minus, Settings } from 'lucide-react'
 import { useChat } from '@/contexts/ChatContext'
+
+function XIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
+    </svg>
+  )
+}
+
+function MinusIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
+    </svg>
+  )
+}
 import { SessionSidebar } from './SessionSidebar'
 import { ChatMessageList } from './ChatMessageList'
 import { ChatInput } from './ChatInput'
@@ -39,14 +54,14 @@ export function ChatWindow() {
             className="rounded p-1.5 text-white/80 hover:bg-white/10 hover:text-white"
             aria-label="Minimize chat"
           >
-            <Minus className="h-4 w-4" />
+            <MinusIcon className="h-4 w-4" />
           </button>
           <button
             onClick={closeChat}
             className="rounded p-1.5 text-white/80 hover:bg-white/10 hover:text-white"
             aria-label="Close chat"
           >
-            <X className="h-4 w-4" />
+            <XIcon className="h-4 w-4" />
           </button>
         </div>
       </div>

+ 47 - 7
webui/src/components/Chat/SessionSidebar.tsx

@@ -1,9 +1,49 @@
 // Session list sidebar panel
 
 import { useState } from 'react'
-import { MessageSquarePlus, Trash2, ChevronRight, ChevronDown, Loader2 } from 'lucide-react'
 import { useChat } from '@/contexts/ChatContext'
 
+function MessageSquarePlusIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v6M9 12h6" />
+    </svg>
+  )
+}
+
+function Trash2Icon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
+    </svg>
+  )
+}
+
+function ChevronRightIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
+    </svg>
+  )
+}
+
+function ChevronDownIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
+    </svg>
+  )
+}
+
+function Loader2Icon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
+    </svg>
+  )
+}
+
 export function SessionSidebar() {
   const [isExpanded, setIsExpanded] = useState(false)
   const [deletingId, setDeletingId] = useState<string | null>(null)
@@ -64,9 +104,9 @@ export function SessionSidebar() {
           Sessions ({sessions.length})
         </span>
         {isExpanded ? (
-          <ChevronDown className="h-4 w-4 text-gray-500" />
+          <ChevronDownIcon className="h-4 w-4 text-gray-500" />
         ) : (
-          <ChevronRight className="h-4 w-4 text-gray-500" />
+          <ChevronRightIcon className="h-4 w-4 text-gray-500" />
         )}
       </button>
 
@@ -78,14 +118,14 @@ export function SessionSidebar() {
             onClick={handleCreateSession}
             className="flex w-full items-center gap-2 px-4 py-2 text-sm text-blue-600 hover:bg-gray-100 dark:text-blue-400 dark:hover:bg-gray-700"
           >
-            <MessageSquarePlus className="h-4 w-4" />
+            <MessageSquarePlusIcon className="h-4 w-4" />
             New conversation
           </button>
 
           {/* Loading state */}
           {isLoadingSessions && (
             <div className="flex items-center justify-center py-4">
-              <Loader2 className="h-5 w-5 animate-spin text-gray-400" />
+              <Loader2Icon className="h-5 w-5 animate-spin text-gray-400" />
             </div>
           )}
 
@@ -115,9 +155,9 @@ export function SessionSidebar() {
                 aria-label="Delete session"
               >
                 {deletingId === session.id ? (
-                  <Loader2 className="h-4 w-4 animate-spin" />
+                  <Loader2Icon className="h-4 w-4 animate-spin" />
                 ) : (
-                  <Trash2 className="h-4 w-4" />
+                  <Trash2Icon className="h-4 w-4" />
                 )}
               </button>
             </div>

+ 66 - 9
webui/src/pages/LlmProviders.tsx

@@ -1,12 +1,69 @@
 // LLM Providers management page
 
 import { useState, useEffect, useCallback } from 'react'
-import { Plus, Trash2, Edit2, RefreshCw, CheckCircle, XCircle, Loader2 } from 'lucide-react'
 import * as chatApi from '@/api/chat'
 import Button from '@/components/Button'
 import Input from '@/components/Input'
 import type { LlmProvider, CreateProviderRequest, UpdateProviderRequest } from '@/types/chat'
 
+// Inline SVG Icons
+function PlusIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
+    </svg>
+  )
+}
+
+function TrashIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
+    </svg>
+  )
+}
+
+function EditIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
+    </svg>
+  )
+}
+
+function RefreshIcon({ className = 'h-4 w-4' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
+    </svg>
+  )
+}
+
+function CheckCircleIcon({ className = 'h-3 w-3' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
+    </svg>
+  )
+}
+
+function XCircleIcon({ className = 'h-3 w-3' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
+      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
+    </svg>
+  )
+}
+
+function LoaderIcon({ className = 'h-8 w-8' }: { className?: string }) {
+  return (
+    <svg className={className} fill="none" viewBox="0 0 24 24">
+      <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
+      <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
+    </svg>
+  )
+}
+
 const PROVIDER_TYPES = [
   { value: 'PROVIDER_TYPE_OPENAI', label: 'OpenAI Compatible', defaultUrl: 'https://api.openai.com/v1' },
   { value: 'PROVIDER_TYPE_ANTHROPIC', label: 'Anthropic Compatible', defaultUrl: 'https://api.anthropic.com/v1' },
@@ -329,7 +386,7 @@ export default function LlmProviders() {
           </p>
         </div>
         <Button onClick={() => setIsModalOpen(true)}>
-          <Plus className="mr-2 h-4 w-4" />
+          <PlusIcon className="mr-2 h-4 w-4" />
           Add Provider
         </Button>
       </div>
@@ -340,13 +397,13 @@ export default function LlmProviders() {
 
       {isLoading ? (
         <div className="mt-8 flex justify-center">
-          <Loader2 className="h-8 w-8 animate-spin text-gray-400" />
+          <LoaderIcon className="h-8 w-8 animate-spin text-gray-400" />
         </div>
       ) : providers.length === 0 ? (
         <div className="mt-8 text-center">
           <p className="text-gray-500">No providers configured yet.</p>
           <Button onClick={() => setIsModalOpen(true)} className="mt-4">
-            <Plus className="mr-2 h-4 w-4" />
+            <PlusIcon className="mr-2 h-4 w-4" />
             Add your first provider
           </Button>
         </div>
@@ -369,12 +426,12 @@ export default function LlmProviders() {
                     {provider.enabled && healthStatus[provider.id] !== undefined && (
                       healthStatus[provider.id]?.healthy ? (
                         <span className="flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700">
-                          <CheckCircle className="h-3 w-3" />
+                          <CheckCircleIcon className="h-3 w-3" />
                           {healthStatus[provider.id]?.latency_ms}ms
                         </span>
                       ) : (
                         <span className="flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs text-red-700">
-                          <XCircle className="h-3 w-3" />
+                          <XCircleIcon className="h-3 w-3" />
                           Unhealthy
                         </span>
                       )
@@ -394,7 +451,7 @@ export default function LlmProviders() {
                     className="rounded p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 disabled:opacity-50"
                     title="Refresh models"
                   >
-                    <RefreshCw className={`h-4 w-4 ${refreshingProvider === provider.id ? 'animate-spin' : ''}`} />
+                    <RefreshIcon className={`h-4 w-4 ${refreshingProvider === provider.id ? 'animate-spin' : ''}`} />
                   </button>
                   <button
                     onClick={() => {
@@ -404,14 +461,14 @@ export default function LlmProviders() {
                     className="rounded p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
                     title="Edit provider"
                   >
-                    <Edit2 className="h-4 w-4" />
+                    <EditIcon className="h-4 w-4" />
                   </button>
                   <button
                     onClick={() => setDeletingProvider(provider)}
                     className="rounded p-2 text-gray-400 hover:bg-red-50 hover:text-red-600"
                     title="Delete provider"
                   >
-                    <Trash2 className="h-4 w-4" />
+                    <TrashIcon className="h-4 w-4" />
                   </button>
                 </div>
               </div>