misc: provider icons, tooltip in chat toolbar, conditionally display the "Open Skills Folder" button and update "Documentation" to "Website" in settings (#60)

This commit is contained in:
Felix
2026-02-12 11:11:28 +08:00
committed by GitHub
Unverified
parent 2ae4201639
commit 8ab1b3af36
26 changed files with 206 additions and 82 deletions

View File

@@ -5,7 +5,7 @@
* Supports: file picker, clipboard paste, drag & drop.
*/
import { useState, useRef, useEffect, useCallback } from 'react';
import { Send, Square, ImagePlus, X } from 'lucide-react';
import { Send, Square, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
@@ -200,18 +200,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
{/* Input Row */}
<div className={`flex items-end gap-2 ${dragOver ? 'ring-2 ring-primary rounded-lg' : ''}`}>
{/* Image Upload Button */}
<Button
type="button"
variant="ghost"
size="icon"
className="shrink-0 h-[44px] w-[44px] text-muted-foreground hover:text-foreground"
onClick={() => fileInputRef.current?.click()}
disabled={disabled}
title="Attach image"
>
<ImagePlus className="h-5 w-5" />
</Button>
<input
ref={fileInputRef}
type="file"

View File

@@ -5,8 +5,10 @@
*/
import { RefreshCw, Brain, ChevronDown, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useChatStore } from '@/stores/chat';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
export function ChatToolbar() {
const sessions = useChatStore((s) => s.sessions);
@@ -17,6 +19,7 @@ export function ChatToolbar() {
const loading = useChatStore((s) => s.loading);
const showThinking = useChatStore((s) => s.showThinking);
const toggleThinking = useChatStore((s) => s.toggleThinking);
const { t } = useTranslation('chat');
const handleSessionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
switchSession(e.target.value);
@@ -51,41 +54,59 @@ export function ChatToolbar() {
</div>
{/* New Session */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={newSession}
title="New session"
>
<Plus className="h-4 w-4" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={newSession}
>
<Plus className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t('toolbar.newSession')}</p>
</TooltipContent>
</Tooltip>
{/* Refresh */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => refresh()}
disabled={loading}
title="Refresh chat"
>
<RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => refresh()}
disabled={loading}
>
<RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t('toolbar.refresh')}</p>
</TooltipContent>
</Tooltip>
{/* Thinking Toggle */}
<Button
variant="ghost"
size="icon"
className={cn(
'h-8 w-8',
showThinking && 'bg-primary/10 text-primary',
)}
onClick={toggleThinking}
title={showThinking ? 'Hide thinking' : 'Show thinking'}
>
<Brain className="h-4 w-4" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
'h-8 w-8',
showThinking && 'bg-primary/10 text-primary',
)}
onClick={toggleThinking}
>
<Brain className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{showThinking ? t('toolbar.hideThinking') : t('toolbar.showThinking')}</p>
</TooltipContent>
</Tooltip>
</div>
);
}

View File

@@ -101,7 +101,7 @@ const defaultSkills: DefaultSkill[] = [
{ id: 'terminal', name: 'Terminal', description: 'Shell command execution' },
];
import { SETUP_PROVIDERS, type ProviderTypeInfo } from '@/lib/providers';
import { SETUP_PROVIDERS, type ProviderTypeInfo, getProviderIconUrl, shouldInvertInDark } from '@/lib/providers';
// Use the shared provider registry for setup providers
const providers = SETUP_PROVIDERS;
@@ -1454,7 +1454,7 @@ function CompleteContent({ selectedProvider, installedSkills }: CompleteContentP
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
<span>{t('complete.provider')}</span>
<span className="text-green-400">
{providerData ? `${providerData.icon} ${providerData.name}` : '—'}
{providerData ? <span className="flex items-center gap-1.5">{getProviderIconUrl(providerData.id) ? <img src={getProviderIconUrl(providerData.id)} alt={providerData.name} className={`h-4 w-4 inline-block ${shouldInvertInDark(providerData.id) ? 'dark:invert' : ''}`} /> : providerData.icon} {providerData.name}</span> : '—'}
</span>
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">

View File

@@ -615,6 +615,8 @@ export function Skills() {
}
}, [enableSkill, disableSkill, t]);
const hasInstalledSkills = skills.some(s => !s.isBundled);
const handleOpenSkillsFolder = useCallback(async () => {
try {
const skillsDir = await window.electron.ipcRenderer.invoke('openclaw:getSkillsDir') as string;
@@ -623,7 +625,12 @@ export function Skills() {
}
const result = await window.electron.ipcRenderer.invoke('shell:openPath', skillsDir) as string;
if (result) {
throw new Error(result);
// shell.openPath returns an error string if the path doesn't exist
if (result.toLowerCase().includes('no such file') || result.toLowerCase().includes('not found') || result.toLowerCase().includes('failed to open')) {
toast.error(t('toast.failedFolderNotFound'));
} else {
throw new Error(result);
}
}
} catch (err) {
toast.error(t('toast.failedOpenFolder') + ': ' + String(err));
@@ -702,10 +709,12 @@ export function Skills() {
<RefreshCw className="h-4 w-4 mr-2" />
{t('refresh')}
</Button>
<Button variant="outline" onClick={handleOpenSkillsFolder}>
<FolderOpen className="h-4 w-4 mr-2" />
{t('openFolder')}
</Button>
{hasInstalledSkills && (
<Button variant="outline" onClick={handleOpenSkillsFolder}>
<FolderOpen className="h-4 w-4 mr-2" />
{t('openFolder')}
</Button>
)}
</div>
</div>